Vibecode Hazel
track this build5 phases, 15 steps, beginner friendly0%The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Hazel, watch selected folders and apply transparent file rules with previews and logs. The hard boundary is mature filesystem automation, rule depth, apple integrations, and reliability, plus os integration polish and maintenance.
You are building a lean indie version of Hazel. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Hazel · indie build A background agent watching folders you choose, matching files against rules you can read, and moving, renaming, tagging or running a script on them, with a dry run that shows exactly what would happen and a log that lets you undo anything it did. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Language | Swift 6 and SwiftUI in Xcode | FSEvents, file coordination and tags are all platform APIs | | App type | A background agent with LSUIElement true | it watches folders; a Dock icon would be noise | | Watching | FSEvents with debounce and a settle check | the supported way to hear about changes without polling a disk | | Storage | Rules and a run log in SQLite under Application Support | the log is what makes undo possible and it needs to be queryable | | Undo | Recorded inverse operations, not a trash can | knowing a file moved from A to B is what lets you put it back | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A Mac running macOS 14 or newer** · free if you own one - Why: FSEvents, file coordination, tags and security scoped bookmarks are all platform APIs. - Get it: Apple menu, then About This Mac. - Verify: About This Mac reports macOS 14 or higher - [ ] **Xcode 16 or newer** · free - Why: Builds and signs the agent. - Get it: Mac App Store or developer.apple.com/xcode. - Verify: xcodebuild -version prints 16 or higher - [ ] **A scratch folder with two hundred throwaway files** · free - Why: Every phase acts on files, and testing against anything you care about is how a bug becomes a loss. - Get it: Generate them with a script so the folder can be recreated instantly after each destructive test. - Verify: A regeneration script recreates the folder from nothing - [ ] **A working backup before you run any rule on real files** · free with hardware you own - Why: This application moves and deletes files. Undo covers what it recorded; it does not cover a bug it did not record. - Get it: Confirm Time Machine or your backup tool has a recent snapshot, and that you have restored from it at least once. - Verify: A test file has been restored from backup successfully - [ ] **Apple Developer Program membership** (optional) · 99 USD per year - Why: Product phase only. Without it there is no notarization and a Gatekeeper warning elsewhere. - Get it: Enrol at developer.apple.com/programs. - Verify: A Developer ID Application certificate appears in Xcode settings ## Quick start ```sh xcrun notarytool submit Rules.zip --keychain-profile AC --wait ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The rule depth accumulated by the commercial product. - Deep Photos and Mail integrations. - Guaranteed compatibility across macOS releases. - Cloud sync of rules. - mature filesystem automation, rule depth, Apple integrations, and reliability - years of OS-version edge cases - large extension ecosystem - cross-device sync - commercial support and rapid compatibility fixes If one of those is essential to you, that is the reason to keep paying for Hazel, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Hazel The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a macOS folder watcher that applies file rules with a preview and a log of everything it did to replace Hazel. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Language: Swift 6 and SwiftUI in Xcode · FSEvents, file coordination and tags are all platform APIs - App type: A background agent with LSUIElement true · it watches folders; a Dock icon would be noise - Watching: FSEvents with debounce and a settle check · the supported way to hear about changes without polling a disk - Storage: Rules and a run log in SQLite under Application Support · the log is what makes undo possible and it needs to be queryable - Undo: Recorded inverse operations, not a trash can · knowing a file moved from A to B is what lets you put it back ### Data model (create this before Phase 1) - folders(id, path, recursive, enabled, bookmark_data) · bookmark_data is a security scoped bookmark so access survives a relaunch - rules(id, folder_id, name, priority, enabled, conditions_json, actions_json, stop_on_match) - runs(id, rule_id, file_path, matched_at, action, from_path, to_path, result, error) · the log and the undo record - quarantine(path, reason, at) · files a rule refused to touch, with why - A rule is conditions and actions as data, never code, so a dry run can evaluate it without doing anything. ### Phase 1 · Watching a folder without hurting yourself Build: Watch a folder with FSEvents. Debounce events, then wait for the file size and modification time to stop changing before considering it settled. Log what you observed and do nothing else. Trap: A file being copied into a watched folder generates events while it is still incomplete, and acting on it moves or renames a partial file. Wait for the size to be stable across two checks a second or so apart before treating a file as ready, and be aware that a very slow copy can still fool a naive settle check. Done when: Copying a large file into the watched folder produces exactly one settled event, after the copy completes, verified by comparing the observed size to the final size. Creating fifty files at once produces fifty events without dropping any. Restarting the application resumes watching without reprocessing existing files. Do not build yet: No rules, no actions. Reliable observation only. ### Phase 2 · Rules as readable data Build: Conditions on name, extension, size, dates, and content type. Actions to move, rename, tag, copy or run a script. Store both as data. Evaluate rules in priority order with an explicit stop-on-match, and build a dry run that reports every match and intended action without touching anything. Trap: A rule engine that executes as it evaluates cannot offer a dry run, and a file automation tool without a dry run will eventually move a thousand files somewhere you did not intend. Separate matching from acting from the beginning; retrofitting that separation means rewriting the engine. Done when: A dry run over a folder of two hundred files reports intended actions and changes nothing, verified by checksumming the folder before and after. Rules evaluate in priority order and stop-on-match prevents later rules firing. A rule with an impossible condition matches nothing rather than everything. Do not build yet: No actions actually run yet. ### Phase 3 · Acting safely, with undo Build: Perform actions through file coordination so other applications see a consistent state. Record every action with enough detail to reverse it. Refuse to overwrite an existing file, and quarantine anything ambiguous rather than guessing. Trap: Moving a file onto an existing path silently destroys the destination in most naive implementations. Refuse the move, record it as a conflict, and let the user decide. A file automation tool that overwrites will eventually overwrite something irreplaceable. Done when: Running a rule that moves files produces the expected result and a log entry per file. Undoing a run returns every file to its original location, verified by checksum and path. A move onto an existing path is refused and quarantined rather than overwriting. A rule that fails partway leaves a log showing exactly what completed. Do not build yet: No scripts and no scheduling yet. ### Phase 4 · Scripts, and the blast radius they bring Build: A script action passing the matched file path as an argument. Run off the main thread with a timeout and kill the process group on expiry. Cap how many files one rule may act on in a single run, and require confirmation above it. Trap: A rule that matches more broadly than you intended, combined with a script action, is how a folder gets reorganised beyond recognition in seconds. A per-run action cap that requires confirmation above a threshold turns a catastrophe into a question. Done when: A script action receives the file path and its output appears in the log. A script that hangs is killed at its timeout and the rule continues with the next file. A rule matching more files than the cap pauses and asks rather than proceeding. Script actions are visibly marked in the rule list. Do not build yet: No scheduling or import yet. ### Phase 5 · Sharing and inspecting rules Build: Export rules as versioned JSON. Import with validation and a preview that lists every action, with script actions called out individually and approved separately. Show a run history with filtering so you can answer what happened to a file. Trap: An imported rule set containing a script action is arbitrary code from whoever wrote it, running against your files on a trigger you may not fully understand. List every script prominently in the import preview and require explicit approval for each. Done when: Exported rules import into a fresh installation and behave identically. An import preview lists every script action separately and each requires approval. Searching the run history for a filename shows every action taken on it with timestamps. Do not build yet: Signing and distribution come last. ### Phase 6 · Signing, notarization and updates Build: Hardened runtime, Developer ID signing, notarization, stapling and Sparkle updates. Document the blast radius honestly: this application moves and deletes files based on rules, and that deserves a plain statement rather than a feature list. Trap: Full Disk Access is required to watch some locations and is a serious permission. Ask for it only when a rule actually needs it, and explain which folder required it, rather than requesting it at first launch for convenience. Done when: A notarized build opens on a fresh Mac with no Gatekeeper warning. Permissions are requested only when a rule requires them, naming the folder. The README states plainly that the application moves and deletes files and how undo works. Do not build yet: Nothing after this. ### Out of scope (and why) - The rule depth of the commercial product, which has accumulated conditions and actions for a decade of specific needs. - Deep Apple integrations such as Photos and Mail rules, each of which is its own framework and its own permission. - Compatibility across major macOS releases without maintenance, since file coordination and permissions both shift. - Cloud sync of rules, which would mean accounts and a server for a settings file. ### README must contain - That the application moves and deletes files, and that dry run and undo are the safety net - How the settle check works and why a slow copy can still fool it - That a move onto an existing path is refused rather than overwriting - The per-run action cap and how to change it - That imported rule sets can contain scripts and each is approved individually ===== AGENTS.md ===== # Agent instructions · Hazel indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Swift 6 and SwiftUI in Xcode, A background agent with LSUIElement true, FSEvents with debounce and a settle check, Rules and a run log in SQLite under Application Support, Recorded inverse operations, not a trash can. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - A file being copied into a watched folder generates events while it is still incomplete, and acting on it moves or renames a partial file. Wait for the size to be stable across two checks a second or so apart before treating a file as ready, and be aware that a very slow copy can still fool a naive settle check. - A rule engine that executes as it evaluates cannot offer a dry run, and a file automation tool without a dry run will eventually move a thousand files somewhere you did not intend. Separate matching from acting from the beginning; retrofitting that separation means rewriting the engine. - Moving a file onto an existing path silently destroys the destination in most naive implementations. Refuse the move, record it as a conflict, and let the user decide. A file automation tool that overwrites will eventually overwrite something irreplaceable. - A rule that matches more broadly than you intended, combined with a script action, is how a folder gets reorganised beyond recognition in seconds. A per-run action cap that requires confirmation above a threshold turns a catastrophe into a question. - An imported rule set containing a script action is arbitrary code from whoever wrote it, running against your files on a trigger you may not fully understand. List every script prominently in the import preview and require explicit approval for each. ===== BUILD_PLAN.md ===== # Build plan · Hazel A background agent watching folders you choose, matching files against rules you can read, and moving, renaming, tagging or running a script on them, with a dry run that shows exactly what would happen and a log that lets you undo anything it did. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Watching a folder without hurting yourself You are told about new and changed files, once each, after they have finished being written. ### Steps 1. Set up the FSEvents stream One stream per watched folder, with a latency that batches bursts. Persist the event identifier so a restart does not replay history. Files: `Watcher.swift` 2. Wait for files to settle Size and modification time stable across two checks. Test with a deliberately slow copy. Files: `Settle.swift` 3. Persist folder access properly Security scoped bookmarks so access to a user-chosen folder survives a relaunch without asking again. Files: `Bookmarks.swift` ### Done when - [ ] A large copy produces one event after it completes - [ ] Fifty simultaneous files produce fifty events - [ ] A restart does not reprocess existing files - [ ] Folder access survives a relaunch without re-prompting ### Watch out - A file being copied into a watched folder generates events while it is still incomplete, and acting on it moves or renames a partial file. Wait for the size to be stable across two checks a second or so apart before treating a file as ready, and be aware that a very slow copy can still fool a naive settle check. ## Phase 2 · Rules as readable data A rule is something you can read, and evaluating it does nothing until you say so. ### Steps 1. Model conditions and actions as data Serialisable structures, not closures. This is what makes dry run and inspection possible. Files: `Rule.swift` 2. Separate match from act One function decides matches; another performs actions. The dry run calls only the first. Files: `Engine.swift` 3. Verify the dry run changes nothing Checksum every file in the folder before and after a dry run. This test is worth more than it looks. Files: `Tests/DryRun.swift` ### Done when - [ ] A dry run reports actions and changes no file, proved by checksums - [ ] Priority order and stop-on-match behave as configured - [ ] An impossible condition matches nothing - [ ] Rules are readable as data in the interface ### Watch out - A rule engine that executes as it evaluates cannot offer a dry run, and a file automation tool without a dry run will eventually move a thousand files somewhere you did not intend. Separate matching from acting from the beginning; retrofitting that separation means rewriting the engine. ## Phase 3 · Acting safely, with undo Rules do their work, and anything they did can be reversed. ### Steps 1. Use file coordination NSFileCoordinator for moves and writes so applications reading the same files see consistency. Files: `Actions.swift` 2. Record reversible operations From path and to path for every action. Undo replays them backwards. Files: `Undo.swift` 3. Refuse rather than overwrite A destination that exists is a conflict, not a decision to make silently. Files: `Actions.swift` ### Done when - [ ] Undo returns every file to its original location - [ ] A move onto an existing path is refused and quarantined - [ ] Each action produces a log entry with both paths - [ ] A partial failure leaves an accurate log of what completed ### Watch out - Moving a file onto an existing path silently destroys the destination in most naive implementations. Refuse the move, record it as a conflict, and let the user decide. A file automation tool that overwrites will eventually overwrite something irreplaceable. ## Phase 4 · Scripts, and the blast radius they bring A rule can run your own script, and a bad script cannot run away with your files. ### Steps 1. Run scripts with a hard timeout Off the main thread, process group killed on expiry, output captured into the log. Files: `ScriptAction.swift` 2. Cap actions per run A threshold above which the run pauses for confirmation. This is the safety net that matters most. Files: `Engine.swift` 3. Mark script rules visibly In the rule list and in the dry run. A script action deserves more attention than a move. Files: `RuleList.swift` ### Done when - [ ] A script receives the file path and its output is logged - [ ] A hanging script is killed and the run continues - [ ] Exceeding the action cap pauses for confirmation - [ ] Script rules are visually distinct in the list ### Watch out - A rule that matches more broadly than you intended, combined with a script action, is how a folder gets reorganised beyond recognition in seconds. A per-run action cap that requires confirmation above a threshold turns a catastrophe into a question. ## Phase 5 · Sharing and inspecting rules Rules can be exported, read and imported without importing a surprise. ### Steps 1. Version the export format A schema version, refused if newer than the application understands. Files: `Export.swift` 2. Preview and approve imports Every action listed, scripts individually approved, and the import applied atomically or not at all. Files: `ImportPreview.swift` 3. Make history searchable By filename, rule and date. Answering what happened to a file is the log's main job. Files: `History.swift` ### Done when - [ ] Exported rules import and behave identically - [ ] Script actions require individual approval on import - [ ] History search by filename shows every action on it - [ ] A failed import leaves existing rules untouched ### Watch out - An imported rule set containing a script action is arbitrary code from whoever wrote it, running against your files on a trigger you may not fully understand. List every script prominently in the import preview and require explicit approval for each. ## Not in this build - The rule depth accumulated by the commercial product. - Deep Photos and Mail integrations. - Guaranteed compatibility across macOS releases. - Cloud sync of rules. ## After v1, if you want it - Content-based conditions such as matching text inside a PDF - A scheduled sweep for folders where watching is impractical - Rule templates for common cases such as sorting downloads by type ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Rules, folders and the run log. RULES_DB_PATH=~/Library/Application Support/Rules/rules.db # Required. How long between size checks before a file counts as settled. Longer is safer for slow copies. SETTLE_CHECK_INTERVAL_MS=1000 # Required. Above this a run pauses for confirmation. The main protection against an over-broad rule. MAX_ACTIONS_PER_RUN=50 # Required. How long a script action may run before it is killed. SCRIPT_TIMEOUT_MS=10000 # Required. How long run history is kept. Undo depends on it, so do not set this short. LOG_RETENTION_DAYS=90
You are building a lean indie version of Hazel. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Hazel · indie build A background agent watching folders you choose, matching files against rules you can read, and moving, renaming, tagging or running a script on them, with a dry run that shows exactly what would happen and a log that lets you undo anything it did. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Language | Swift 6 and SwiftUI in Xcode | FSEvents, file coordination and tags are all platform APIs | | App type | A background agent with LSUIElement true | it watches folders; a Dock icon would be noise | | Watching | FSEvents with debounce and a settle check | the supported way to hear about changes without polling a disk | | Storage | Rules and a run log in SQLite under Application Support | the log is what makes undo possible and it needs to be queryable | | Undo | Recorded inverse operations, not a trash can | knowing a file moved from A to B is what lets you put it back | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A Mac running macOS 14 or newer** · free if you own one - Why: FSEvents, file coordination, tags and security scoped bookmarks are all platform APIs. - Get it: Apple menu, then About This Mac. - Verify: About This Mac reports macOS 14 or higher - [ ] **Xcode 16 or newer** · free - Why: Builds and signs the agent. - Get it: Mac App Store or developer.apple.com/xcode. - Verify: xcodebuild -version prints 16 or higher - [ ] **A scratch folder with two hundred throwaway files** · free - Why: Every phase acts on files, and testing against anything you care about is how a bug becomes a loss. - Get it: Generate them with a script so the folder can be recreated instantly after each destructive test. - Verify: A regeneration script recreates the folder from nothing - [ ] **A working backup before you run any rule on real files** · free with hardware you own - Why: This application moves and deletes files. Undo covers what it recorded; it does not cover a bug it did not record. - Get it: Confirm Time Machine or your backup tool has a recent snapshot, and that you have restored from it at least once. - Verify: A test file has been restored from backup successfully - [ ] **Apple Developer Program membership** (optional) · 99 USD per year - Why: Product phase only. Without it there is no notarization and a Gatekeeper warning elsewhere. - Get it: Enrol at developer.apple.com/programs. - Verify: A Developer ID Application certificate appears in Xcode settings ## Quick start ```sh xcrun notarytool submit Rules.zip --keychain-profile AC --wait ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The rule depth accumulated by the commercial product. - Deep Photos and Mail integrations. - Guaranteed compatibility across macOS releases. - Cloud sync of rules. - mature filesystem automation, rule depth, Apple integrations, and reliability - years of OS-version edge cases - large extension ecosystem - cross-device sync - commercial support and rapid compatibility fixes If one of those is essential to you, that is the reason to keep paying for Hazel, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Hazel The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a macOS folder watcher that applies file rules with a preview and a log of everything it did to replace Hazel. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Language: Swift 6 and SwiftUI in Xcode · FSEvents, file coordination and tags are all platform APIs - App type: A background agent with LSUIElement true · it watches folders; a Dock icon would be noise - Watching: FSEvents with debounce and a settle check · the supported way to hear about changes without polling a disk - Storage: Rules and a run log in SQLite under Application Support · the log is what makes undo possible and it needs to be queryable - Undo: Recorded inverse operations, not a trash can · knowing a file moved from A to B is what lets you put it back ### Data model (create this before Phase 1) - folders(id, path, recursive, enabled, bookmark_data) · bookmark_data is a security scoped bookmark so access survives a relaunch - rules(id, folder_id, name, priority, enabled, conditions_json, actions_json, stop_on_match) - runs(id, rule_id, file_path, matched_at, action, from_path, to_path, result, error) · the log and the undo record - quarantine(path, reason, at) · files a rule refused to touch, with why - A rule is conditions and actions as data, never code, so a dry run can evaluate it without doing anything. ### Phase 1 · Watching a folder without hurting yourself Build: Watch a folder with FSEvents. Debounce events, then wait for the file size and modification time to stop changing before considering it settled. Log what you observed and do nothing else. Trap: A file being copied into a watched folder generates events while it is still incomplete, and acting on it moves or renames a partial file. Wait for the size to be stable across two checks a second or so apart before treating a file as ready, and be aware that a very slow copy can still fool a naive settle check. Done when: Copying a large file into the watched folder produces exactly one settled event, after the copy completes, verified by comparing the observed size to the final size. Creating fifty files at once produces fifty events without dropping any. Restarting the application resumes watching without reprocessing existing files. Do not build yet: No rules, no actions. Reliable observation only. ### Phase 2 · Rules as readable data Build: Conditions on name, extension, size, dates, and content type. Actions to move, rename, tag, copy or run a script. Store both as data. Evaluate rules in priority order with an explicit stop-on-match, and build a dry run that reports every match and intended action without touching anything. Trap: A rule engine that executes as it evaluates cannot offer a dry run, and a file automation tool without a dry run will eventually move a thousand files somewhere you did not intend. Separate matching from acting from the beginning; retrofitting that separation means rewriting the engine. Done when: A dry run over a folder of two hundred files reports intended actions and changes nothing, verified by checksumming the folder before and after. Rules evaluate in priority order and stop-on-match prevents later rules firing. A rule with an impossible condition matches nothing rather than everything. Do not build yet: No actions actually run yet. ### Phase 3 · Acting safely, with undo Build: Perform actions through file coordination so other applications see a consistent state. Record every action with enough detail to reverse it. Refuse to overwrite an existing file, and quarantine anything ambiguous rather than guessing. Trap: Moving a file onto an existing path silently destroys the destination in most naive implementations. Refuse the move, record it as a conflict, and let the user decide. A file automation tool that overwrites will eventually overwrite something irreplaceable. Done when: Running a rule that moves files produces the expected result and a log entry per file. Undoing a run returns every file to its original location, verified by checksum and path. A move onto an existing path is refused and quarantined rather than overwriting. A rule that fails partway leaves a log showing exactly what completed. Do not build yet: No scripts and no scheduling yet. ### Phase 4 · Scripts, and the blast radius they bring Build: A script action passing the matched file path as an argument. Run off the main thread with a timeout and kill the process group on expiry. Cap how many files one rule may act on in a single run, and require confirmation above it. Trap: A rule that matches more broadly than you intended, combined with a script action, is how a folder gets reorganised beyond recognition in seconds. A per-run action cap that requires confirmation above a threshold turns a catastrophe into a question. Done when: A script action receives the file path and its output appears in the log. A script that hangs is killed at its timeout and the rule continues with the next file. A rule matching more files than the cap pauses and asks rather than proceeding. Script actions are visibly marked in the rule list. Do not build yet: No scheduling or import yet. ### Phase 5 · Sharing and inspecting rules Build: Export rules as versioned JSON. Import with validation and a preview that lists every action, with script actions called out individually and approved separately. Show a run history with filtering so you can answer what happened to a file. Trap: An imported rule set containing a script action is arbitrary code from whoever wrote it, running against your files on a trigger you may not fully understand. List every script prominently in the import preview and require explicit approval for each. Done when: Exported rules import into a fresh installation and behave identically. An import preview lists every script action separately and each requires approval. Searching the run history for a filename shows every action taken on it with timestamps. Do not build yet: Signing and distribution come last. ### Phase 6 · Signing, notarization and updates Build: Hardened runtime, Developer ID signing, notarization, stapling and Sparkle updates. Document the blast radius honestly: this application moves and deletes files based on rules, and that deserves a plain statement rather than a feature list. Trap: Full Disk Access is required to watch some locations and is a serious permission. Ask for it only when a rule actually needs it, and explain which folder required it, rather than requesting it at first launch for convenience. Done when: A notarized build opens on a fresh Mac with no Gatekeeper warning. Permissions are requested only when a rule requires them, naming the folder. The README states plainly that the application moves and deletes files and how undo works. Do not build yet: Nothing after this. ### Out of scope (and why) - The rule depth of the commercial product, which has accumulated conditions and actions for a decade of specific needs. - Deep Apple integrations such as Photos and Mail rules, each of which is its own framework and its own permission. - Compatibility across major macOS releases without maintenance, since file coordination and permissions both shift. - Cloud sync of rules, which would mean accounts and a server for a settings file. ### README must contain - That the application moves and deletes files, and that dry run and undo are the safety net - How the settle check works and why a slow copy can still fool it - That a move onto an existing path is refused rather than overwriting - The per-run action cap and how to change it - That imported rule sets can contain scripts and each is approved individually ===== AGENTS.md ===== # Agent instructions · Hazel indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Swift 6 and SwiftUI in Xcode, A background agent with LSUIElement true, FSEvents with debounce and a settle check, Rules and a run log in SQLite under Application Support, Recorded inverse operations, not a trash can. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - A file being copied into a watched folder generates events while it is still incomplete, and acting on it moves or renames a partial file. Wait for the size to be stable across two checks a second or so apart before treating a file as ready, and be aware that a very slow copy can still fool a naive settle check. - A rule engine that executes as it evaluates cannot offer a dry run, and a file automation tool without a dry run will eventually move a thousand files somewhere you did not intend. Separate matching from acting from the beginning; retrofitting that separation means rewriting the engine. - Moving a file onto an existing path silently destroys the destination in most naive implementations. Refuse the move, record it as a conflict, and let the user decide. A file automation tool that overwrites will eventually overwrite something irreplaceable. - A rule that matches more broadly than you intended, combined with a script action, is how a folder gets reorganised beyond recognition in seconds. A per-run action cap that requires confirmation above a threshold turns a catastrophe into a question. - An imported rule set containing a script action is arbitrary code from whoever wrote it, running against your files on a trigger you may not fully understand. List every script prominently in the import preview and require explicit approval for each. ===== BUILD_PLAN.md ===== # Build plan · Hazel A background agent watching folders you choose, matching files against rules you can read, and moving, renaming, tagging or running a script on them, with a dry run that shows exactly what would happen and a log that lets you undo anything it did. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Watching a folder without hurting yourself You are told about new and changed files, once each, after they have finished being written. ### Steps 1. Set up the FSEvents stream One stream per watched folder, with a latency that batches bursts. Persist the event identifier so a restart does not replay history. Files: `Watcher.swift` 2. Wait for files to settle Size and modification time stable across two checks. Test with a deliberately slow copy. Files: `Settle.swift` 3. Persist folder access properly Security scoped bookmarks so access to a user-chosen folder survives a relaunch without asking again. Files: `Bookmarks.swift` ### Done when - [ ] A large copy produces one event after it completes - [ ] Fifty simultaneous files produce fifty events - [ ] A restart does not reprocess existing files - [ ] Folder access survives a relaunch without re-prompting ### Watch out - A file being copied into a watched folder generates events while it is still incomplete, and acting on it moves or renames a partial file. Wait for the size to be stable across two checks a second or so apart before treating a file as ready, and be aware that a very slow copy can still fool a naive settle check. ## Phase 2 · Rules as readable data A rule is something you can read, and evaluating it does nothing until you say so. ### Steps 1. Model conditions and actions as data Serialisable structures, not closures. This is what makes dry run and inspection possible. Files: `Rule.swift` 2. Separate match from act One function decides matches; another performs actions. The dry run calls only the first. Files: `Engine.swift` 3. Verify the dry run changes nothing Checksum every file in the folder before and after a dry run. This test is worth more than it looks. Files: `Tests/DryRun.swift` ### Done when - [ ] A dry run reports actions and changes no file, proved by checksums - [ ] Priority order and stop-on-match behave as configured - [ ] An impossible condition matches nothing - [ ] Rules are readable as data in the interface ### Watch out - A rule engine that executes as it evaluates cannot offer a dry run, and a file automation tool without a dry run will eventually move a thousand files somewhere you did not intend. Separate matching from acting from the beginning; retrofitting that separation means rewriting the engine. ## Phase 3 · Acting safely, with undo Rules do their work, and anything they did can be reversed. ### Steps 1. Use file coordination NSFileCoordinator for moves and writes so applications reading the same files see consistency. Files: `Actions.swift` 2. Record reversible operations From path and to path for every action. Undo replays them backwards. Files: `Undo.swift` 3. Refuse rather than overwrite A destination that exists is a conflict, not a decision to make silently. Files: `Actions.swift` ### Done when - [ ] Undo returns every file to its original location - [ ] A move onto an existing path is refused and quarantined - [ ] Each action produces a log entry with both paths - [ ] A partial failure leaves an accurate log of what completed ### Watch out - Moving a file onto an existing path silently destroys the destination in most naive implementations. Refuse the move, record it as a conflict, and let the user decide. A file automation tool that overwrites will eventually overwrite something irreplaceable. ## Phase 4 · Scripts, and the blast radius they bring A rule can run your own script, and a bad script cannot run away with your files. ### Steps 1. Run scripts with a hard timeout Off the main thread, process group killed on expiry, output captured into the log. Files: `ScriptAction.swift` 2. Cap actions per run A threshold above which the run pauses for confirmation. This is the safety net that matters most. Files: `Engine.swift` 3. Mark script rules visibly In the rule list and in the dry run. A script action deserves more attention than a move. Files: `RuleList.swift` ### Done when - [ ] A script receives the file path and its output is logged - [ ] A hanging script is killed and the run continues - [ ] Exceeding the action cap pauses for confirmation - [ ] Script rules are visually distinct in the list ### Watch out - A rule that matches more broadly than you intended, combined with a script action, is how a folder gets reorganised beyond recognition in seconds. A per-run action cap that requires confirmation above a threshold turns a catastrophe into a question. ## Phase 5 · Sharing and inspecting rules Rules can be exported, read and imported without importing a surprise. ### Steps 1. Version the export format A schema version, refused if newer than the application understands. Files: `Export.swift` 2. Preview and approve imports Every action listed, scripts individually approved, and the import applied atomically or not at all. Files: `ImportPreview.swift` 3. Make history searchable By filename, rule and date. Answering what happened to a file is the log's main job. Files: `History.swift` ### Done when - [ ] Exported rules import and behave identically - [ ] Script actions require individual approval on import - [ ] History search by filename shows every action on it - [ ] A failed import leaves existing rules untouched ### Watch out - An imported rule set containing a script action is arbitrary code from whoever wrote it, running against your files on a trigger you may not fully understand. List every script prominently in the import preview and require explicit approval for each. ## Not in this build - The rule depth accumulated by the commercial product. - Deep Photos and Mail integrations. - Guaranteed compatibility across macOS releases. - Cloud sync of rules. ## After v1, if you want it - Content-based conditions such as matching text inside a PDF - A scheduled sweep for folders where watching is impractical - Rule templates for common cases such as sorting downloads by type ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Rules, folders and the run log. RULES_DB_PATH=~/Library/Application Support/Rules/rules.db # Required. How long between size checks before a file counts as settled. Longer is safer for slow copies. SETTLE_CHECK_INTERVAL_MS=1000 # Required. Above this a run pauses for confirmation. The main protection against an over-broad rule. MAX_ACTIONS_PER_RUN=50 # Required. How long a script action may run before it is killed. SCRIPT_TIMEOUT_MS=10000 # Required. How long run history is kept. Undo depends on it, so do not set this short. LOG_RETENTION_DAYS=90
You are building a production product version of Hazel. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Hazel · product brief ## Problem The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Hazel, watch selected folders and apply transparent file rules with previews and logs. The hard boundary is mature filesystem automation, rule depth, apple integrations, and reliability, plus os integration polish and maintenance. ## Product outcome A file automation tool whose dry run is trustworthy, whose actions are reversible, which refuses to overwrite, which pauses before doing something at scale, and whose log answers what happened to any file. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - macOS 14 or later - Xcode - only the permissions required by the selected feature ## Explicit non-goals for v1 - The rule depth accumulated by the commercial product. - Deep Photos and Mail integrations. - Guaranteed compatibility across macOS releases. - Cloud sync of rules. - mature filesystem automation, rule depth, Apple integrations, and reliability - years of OS-version edge cases - large extension ecosystem - cross-device sync - commercial support and rapid compatibility fixes ## Success criteria - The dry run checksum test confirms nothing changes - Undo is verified to restore every file from a real run - A move onto an existing path is confirmed refused - The action cap is confirmed to pause on an over-broad rule - Permissions are requested per rule rather than at launch ===== BRIEF.md ===== # Build brief · Hazel The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a macOS folder watcher that applies file rules with a preview and a log of everything it did to replace Hazel. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Language: Swift 6 and SwiftUI in Xcode · FSEvents, file coordination and tags are all platform APIs - App type: A background agent with LSUIElement true · it watches folders; a Dock icon would be noise - Watching: FSEvents with debounce and a settle check · the supported way to hear about changes without polling a disk - Storage: Rules and a run log in SQLite under Application Support · the log is what makes undo possible and it needs to be queryable - Undo: Recorded inverse operations, not a trash can · knowing a file moved from A to B is what lets you put it back ### Data model (create this before Phase 1) - folders(id, path, recursive, enabled, bookmark_data) · bookmark_data is a security scoped bookmark so access survives a relaunch - rules(id, folder_id, name, priority, enabled, conditions_json, actions_json, stop_on_match) - runs(id, rule_id, file_path, matched_at, action, from_path, to_path, result, error) · the log and the undo record - quarantine(path, reason, at) · files a rule refused to touch, with why - A rule is conditions and actions as data, never code, so a dry run can evaluate it without doing anything. ### Phase 1 · Watching a folder without hurting yourself Build: Watch a folder with FSEvents. Debounce events, then wait for the file size and modification time to stop changing before considering it settled. Log what you observed and do nothing else. Trap: A file being copied into a watched folder generates events while it is still incomplete, and acting on it moves or renames a partial file. Wait for the size to be stable across two checks a second or so apart before treating a file as ready, and be aware that a very slow copy can still fool a naive settle check. Done when: Copying a large file into the watched folder produces exactly one settled event, after the copy completes, verified by comparing the observed size to the final size. Creating fifty files at once produces fifty events without dropping any. Restarting the application resumes watching without reprocessing existing files. Do not build yet: No rules, no actions. Reliable observation only. ### Phase 2 · Rules as readable data Build: Conditions on name, extension, size, dates, and content type. Actions to move, rename, tag, copy or run a script. Store both as data. Evaluate rules in priority order with an explicit stop-on-match, and build a dry run that reports every match and intended action without touching anything. Trap: A rule engine that executes as it evaluates cannot offer a dry run, and a file automation tool without a dry run will eventually move a thousand files somewhere you did not intend. Separate matching from acting from the beginning; retrofitting that separation means rewriting the engine. Done when: A dry run over a folder of two hundred files reports intended actions and changes nothing, verified by checksumming the folder before and after. Rules evaluate in priority order and stop-on-match prevents later rules firing. A rule with an impossible condition matches nothing rather than everything. Do not build yet: No actions actually run yet. ### Phase 3 · Acting safely, with undo Build: Perform actions through file coordination so other applications see a consistent state. Record every action with enough detail to reverse it. Refuse to overwrite an existing file, and quarantine anything ambiguous rather than guessing. Trap: Moving a file onto an existing path silently destroys the destination in most naive implementations. Refuse the move, record it as a conflict, and let the user decide. A file automation tool that overwrites will eventually overwrite something irreplaceable. Done when: Running a rule that moves files produces the expected result and a log entry per file. Undoing a run returns every file to its original location, verified by checksum and path. A move onto an existing path is refused and quarantined rather than overwriting. A rule that fails partway leaves a log showing exactly what completed. Do not build yet: No scripts and no scheduling yet. ### Phase 4 · Scripts, and the blast radius they bring Build: A script action passing the matched file path as an argument. Run off the main thread with a timeout and kill the process group on expiry. Cap how many files one rule may act on in a single run, and require confirmation above it. Trap: A rule that matches more broadly than you intended, combined with a script action, is how a folder gets reorganised beyond recognition in seconds. A per-run action cap that requires confirmation above a threshold turns a catastrophe into a question. Done when: A script action receives the file path and its output appears in the log. A script that hangs is killed at its timeout and the rule continues with the next file. A rule matching more files than the cap pauses and asks rather than proceeding. Script actions are visibly marked in the rule list. Do not build yet: No scheduling or import yet. ### Phase 5 · Sharing and inspecting rules Build: Export rules as versioned JSON. Import with validation and a preview that lists every action, with script actions called out individually and approved separately. Show a run history with filtering so you can answer what happened to a file. Trap: An imported rule set containing a script action is arbitrary code from whoever wrote it, running against your files on a trigger you may not fully understand. List every script prominently in the import preview and require explicit approval for each. Done when: Exported rules import into a fresh installation and behave identically. An import preview lists every script action separately and each requires approval. Searching the run history for a filename shows every action taken on it with timestamps. Do not build yet: Signing and distribution come last. ### Phase 6 · Signing, notarization and updates Build: Hardened runtime, Developer ID signing, notarization, stapling and Sparkle updates. Document the blast radius honestly: this application moves and deletes files based on rules, and that deserves a plain statement rather than a feature list. Trap: Full Disk Access is required to watch some locations and is a serious permission. Ask for it only when a rule actually needs it, and explain which folder required it, rather than requesting it at first launch for convenience. Done when: A notarized build opens on a fresh Mac with no Gatekeeper warning. Permissions are requested only when a rule requires them, naming the folder. The README states plainly that the application moves and deletes files and how undo works. Do not build yet: Nothing after this. ### Out of scope (and why) - The rule depth of the commercial product, which has accumulated conditions and actions for a decade of specific needs. - Deep Apple integrations such as Photos and Mail rules, each of which is its own framework and its own permission. - Compatibility across major macOS releases without maintenance, since file coordination and permissions both shift. - Cloud sync of rules, which would mean accounts and a server for a settings file. ### README must contain - That the application moves and deletes files, and that dry run and undo are the safety net - How the settle check works and why a slow copy can still fool it - That a move onto an existing path is refused rather than overwriting - The per-run action cap and how to change it - That imported rule sets can contain scripts and each is approved individually ===== ARCHITECTURE.md ===== # Architecture · Hazel ## Stack | Part | Choice | Why | | --- | --- | --- | | Language | Swift 6 and SwiftUI in Xcode | FSEvents, file coordination and tags are all platform APIs | | App type | A background agent with LSUIElement true | it watches folders; a Dock icon would be noise | | Watching | FSEvents with debounce and a settle check | the supported way to hear about changes without polling a disk | | Storage | Rules and a run log in SQLite under Application Support | the log is what makes undo possible and it needs to be queryable | | Undo | Recorded inverse operations, not a trash can | knowing a file moved from A to B is what lets you put it back | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Watcher | Noticing changes and deciding a file has settled. | FSEvents is the platform answer. The settle check is the part that needs care and testing. | | Engine | Matching rules to files. | Pure evaluation over data, testable with no filesystem at all, and the separation from acting is what makes dry run possible. | | Actions | Performing and recording operations. | Each action kind is independent. The refusal to overwrite and the recording of inverses apply to all of them. | | Undo | Reversing recorded operations. | Depends entirely on the log being complete. Any new action kind must record its inverse or undo silently becomes partial. | | Config | Rules, export and import. | JSON so rule sets are readable before they are trusted. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `RULES_DB_PATH` · required · Rules, folders and the run log. - `SETTLE_CHECK_INTERVAL_MS` · required · How long between size checks before a file counts as settled. Longer is safer for slow copies. - `MAX_ACTIONS_PER_RUN` · required · Above this a run pauses for confirmation. The main protection against an over-broad rule. - `SCRIPT_TIMEOUT_MS` · required · How long a script action may run before it is killed. - `LOG_RETENTION_DAYS` · required · How long run history is kept. Undo depends on it, so do not set this short. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Hazel product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Swift 6 and SwiftUI in Xcode, A background agent with LSUIElement true, FSEvents with debounce and a settle check, Rules and a run log in SQLite under Application Support, Recorded inverse operations, not a trash can. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - A file being copied into a watched folder generates events while it is still incomplete, and acting on it moves or renames a partial file. Wait for the size to be stable across two checks a second or so apart before treating a file as ready, and be aware that a very slow copy can still fool a naive settle check. - A rule engine that executes as it evaluates cannot offer a dry run, and a file automation tool without a dry run will eventually move a thousand files somewhere you did not intend. Separate matching from acting from the beginning; retrofitting that separation means rewriting the engine. - Moving a file onto an existing path silently destroys the destination in most naive implementations. Refuse the move, record it as a conflict, and let the user decide. A file automation tool that overwrites will eventually overwrite something irreplaceable. - A rule that matches more broadly than you intended, combined with a script action, is how a folder gets reorganised beyond recognition in seconds. A per-run action cap that requires confirmation above a threshold turns a catastrophe into a question. - An imported rule set containing a script action is arbitrary code from whoever wrote it, running against your files on a trigger you may not fully understand. List every script prominently in the import preview and require explicit approval for each. - Full Disk Access is required to watch some locations and is a serious permission. Ask for it only when a rule actually needs it, and explain which folder required it, rather than requesting it at first launch for convenience. ===== MILESTONES.md ===== # Delivery milestones · Hazel Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Watching a folder without hurting yourself You are told about new and changed files, once each, after they have finished being written. ### Steps 1. Set up the FSEvents stream One stream per watched folder, with a latency that batches bursts. Persist the event identifier so a restart does not replay history. Files: `Watcher.swift` 2. Wait for files to settle Size and modification time stable across two checks. Test with a deliberately slow copy. Files: `Settle.swift` 3. Persist folder access properly Security scoped bookmarks so access to a user-chosen folder survives a relaunch without asking again. Files: `Bookmarks.swift` ### Done when - [ ] A large copy produces one event after it completes - [ ] Fifty simultaneous files produce fifty events - [ ] A restart does not reprocess existing files - [ ] Folder access survives a relaunch without re-prompting ### Watch out - A file being copied into a watched folder generates events while it is still incomplete, and acting on it moves or renames a partial file. Wait for the size to be stable across two checks a second or so apart before treating a file as ready, and be aware that a very slow copy can still fool a naive settle check. ## M2 · Rules as readable data A rule is something you can read, and evaluating it does nothing until you say so. ### Steps 1. Model conditions and actions as data Serialisable structures, not closures. This is what makes dry run and inspection possible. Files: `Rule.swift` 2. Separate match from act One function decides matches; another performs actions. The dry run calls only the first. Files: `Engine.swift` 3. Verify the dry run changes nothing Checksum every file in the folder before and after a dry run. This test is worth more than it looks. Files: `Tests/DryRun.swift` ### Done when - [ ] A dry run reports actions and changes no file, proved by checksums - [ ] Priority order and stop-on-match behave as configured - [ ] An impossible condition matches nothing - [ ] Rules are readable as data in the interface ### Watch out - A rule engine that executes as it evaluates cannot offer a dry run, and a file automation tool without a dry run will eventually move a thousand files somewhere you did not intend. Separate matching from acting from the beginning; retrofitting that separation means rewriting the engine. ## M3 · Acting safely, with undo Rules do their work, and anything they did can be reversed. ### Steps 1. Use file coordination NSFileCoordinator for moves and writes so applications reading the same files see consistency. Files: `Actions.swift` 2. Record reversible operations From path and to path for every action. Undo replays them backwards. Files: `Undo.swift` 3. Refuse rather than overwrite A destination that exists is a conflict, not a decision to make silently. Files: `Actions.swift` ### Done when - [ ] Undo returns every file to its original location - [ ] A move onto an existing path is refused and quarantined - [ ] Each action produces a log entry with both paths - [ ] A partial failure leaves an accurate log of what completed ### Watch out - Moving a file onto an existing path silently destroys the destination in most naive implementations. Refuse the move, record it as a conflict, and let the user decide. A file automation tool that overwrites will eventually overwrite something irreplaceable. ## M4 · Scripts, and the blast radius they bring A rule can run your own script, and a bad script cannot run away with your files. ### Steps 1. Run scripts with a hard timeout Off the main thread, process group killed on expiry, output captured into the log. Files: `ScriptAction.swift` 2. Cap actions per run A threshold above which the run pauses for confirmation. This is the safety net that matters most. Files: `Engine.swift` 3. Mark script rules visibly In the rule list and in the dry run. A script action deserves more attention than a move. Files: `RuleList.swift` ### Done when - [ ] A script receives the file path and its output is logged - [ ] A hanging script is killed and the run continues - [ ] Exceeding the action cap pauses for confirmation - [ ] Script rules are visually distinct in the list ### Watch out - A rule that matches more broadly than you intended, combined with a script action, is how a folder gets reorganised beyond recognition in seconds. A per-run action cap that requires confirmation above a threshold turns a catastrophe into a question. ## M5 · Sharing and inspecting rules Rules can be exported, read and imported without importing a surprise. ### Steps 1. Version the export format A schema version, refused if newer than the application understands. Files: `Export.swift` 2. Preview and approve imports Every action listed, scripts individually approved, and the import applied atomically or not at all. Files: `ImportPreview.swift` 3. Make history searchable By filename, rule and date. Answering what happened to a file is the log's main job. Files: `History.swift` ### Done when - [ ] Exported rules import and behave identically - [ ] Script actions require individual approval on import - [ ] History search by filename shows every action on it - [ ] A failed import leaves existing rules untouched ### Watch out - An imported rule set containing a script action is arbitrary code from whoever wrote it, running against your files on a trigger you may not fully understand. List every script prominently in the import preview and require explicit approval for each. ## M6 · Signing, notarization and updates (production only) Others can install it and trust what it will do to their files. ### Steps 1. Sign and notarize Hardened runtime, Developer ID, notarytool, stapler. ```sh xcrun notarytool submit Rules.zip --keychain-profile AC --wait ``` 2. Request permissions lazily Only when a rule needs the location, and say which rule. A first-launch request for everything trains people to click through. Files: `Permissions.swift` 3. State the blast radius One paragraph: this moves and deletes files, here is the dry run, here is undo, here is the cap. Files: `README.md` ### Done when - [ ] spctl reports the notarized build accepted - [ ] Permissions are requested per rule with the folder named - [ ] The README states what the application can do to files - [ ] An appcast update installs and relaunches ### Watch out - Full Disk Access is required to watch some locations and is a serious permission. Ask for it only when a rule actually needs it, and explain which folder required it, rather than requesting it at first launch for convenience. ===== OPERATIONS.md ===== # Operations · Hazel ## Backup The rules database holds both configuration and the undo log, so losing it loses your ability to reverse recent runs. It is small and belongs in an ordinary backup. ## Restore Replace the database and relaunch. Rules referencing folders that no longer exist should be disabled rather than erroring, so a restore onto a different machine degrades gracefully. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Watch the quarantine count and the rate of refused overwrites. A rising count usually means a rule has become over-broad as the folder's contents changed, which is exactly when it is about to do damage. ## Incident checklist If a rule ran too widely, undo the run from the log first and diagnose afterwards. The action cap should have prevented it; if it did not, that is the bug to fix before re-enabling the rule. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] The dry run checksum test confirms nothing changes - [ ] Undo is verified to restore every file from a real run - [ ] A move onto an existing path is confirmed refused - [ ] The action cap is confirmed to pause on an over-broad rule - [ ] Permissions are requested per rule rather than at launch ## Launch constraint Do not market omitted Hazel capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Rules, folders and the run log. RULES_DB_PATH=~/Library/Application Support/Rules/rules.db # Required. How long between size checks before a file counts as settled. Longer is safer for slow copies. SETTLE_CHECK_INTERVAL_MS=1000 # Required. Above this a run pauses for confirmation. The main protection against an over-broad rule. MAX_ACTIONS_PER_RUN=50 # Required. How long a script action may run before it is killed. SCRIPT_TIMEOUT_MS=10000 # Required. How long run history is kept. Undo depends on it, so do not set this short. LOG_RETENTION_DAYS=90
# Hazel · indie build A background agent watching folders you choose, matching files against rules you can read, and moving, renaming, tagging or running a script on them, with a dry run that shows exactly what would happen and a log that lets you undo anything it did. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Language | Swift 6 and SwiftUI in Xcode | FSEvents, file coordination and tags are all platform APIs | | App type | A background agent with LSUIElement true | it watches folders; a Dock icon would be noise | | Watching | FSEvents with debounce and a settle check | the supported way to hear about changes without polling a disk | | Storage | Rules and a run log in SQLite under Application Support | the log is what makes undo possible and it needs to be queryable | | Undo | Recorded inverse operations, not a trash can | knowing a file moved from A to B is what lets you put it back | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A Mac running macOS 14 or newer** · free if you own one - Why: FSEvents, file coordination, tags and security scoped bookmarks are all platform APIs. - Get it: Apple menu, then About This Mac. - Verify: About This Mac reports macOS 14 or higher - [ ] **Xcode 16 or newer** · free - Why: Builds and signs the agent. - Get it: Mac App Store or developer.apple.com/xcode. - Verify: xcodebuild -version prints 16 or higher - [ ] **A scratch folder with two hundred throwaway files** · free - Why: Every phase acts on files, and testing against anything you care about is how a bug becomes a loss. - Get it: Generate them with a script so the folder can be recreated instantly after each destructive test. - Verify: A regeneration script recreates the folder from nothing - [ ] **A working backup before you run any rule on real files** · free with hardware you own - Why: This application moves and deletes files. Undo covers what it recorded; it does not cover a bug it did not record. - Get it: Confirm Time Machine or your backup tool has a recent snapshot, and that you have restored from it at least once. - Verify: A test file has been restored from backup successfully - [ ] **Apple Developer Program membership** (optional) · 99 USD per year - Why: Product phase only. Without it there is no notarization and a Gatekeeper warning elsewhere. - Get it: Enrol at developer.apple.com/programs. - Verify: A Developer ID Application certificate appears in Xcode settings ## Quick start ```sh xcrun notarytool submit Rules.zip --keychain-profile AC --wait ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The rule depth accumulated by the commercial product. - Deep Photos and Mail integrations. - Guaranteed compatibility across macOS releases. - Cloud sync of rules. - mature filesystem automation, rule depth, Apple integrations, and reliability - years of OS-version edge cases - large extension ecosystem - cross-device sync - commercial support and rapid compatibility fixes If one of those is essential to you, that is the reason to keep paying for Hazel, and the README should say so rather than pretend.
# Build brief · Hazel The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a macOS folder watcher that applies file rules with a preview and a log of everything it did to replace Hazel. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Language: Swift 6 and SwiftUI in Xcode · FSEvents, file coordination and tags are all platform APIs - App type: A background agent with LSUIElement true · it watches folders; a Dock icon would be noise - Watching: FSEvents with debounce and a settle check · the supported way to hear about changes without polling a disk - Storage: Rules and a run log in SQLite under Application Support · the log is what makes undo possible and it needs to be queryable - Undo: Recorded inverse operations, not a trash can · knowing a file moved from A to B is what lets you put it back ### Data model (create this before Phase 1) - folders(id, path, recursive, enabled, bookmark_data) · bookmark_data is a security scoped bookmark so access survives a relaunch - rules(id, folder_id, name, priority, enabled, conditions_json, actions_json, stop_on_match) - runs(id, rule_id, file_path, matched_at, action, from_path, to_path, result, error) · the log and the undo record - quarantine(path, reason, at) · files a rule refused to touch, with why - A rule is conditions and actions as data, never code, so a dry run can evaluate it without doing anything. ### Phase 1 · Watching a folder without hurting yourself Build: Watch a folder with FSEvents. Debounce events, then wait for the file size and modification time to stop changing before considering it settled. Log what you observed and do nothing else. Trap: A file being copied into a watched folder generates events while it is still incomplete, and acting on it moves or renames a partial file. Wait for the size to be stable across two checks a second or so apart before treating a file as ready, and be aware that a very slow copy can still fool a naive settle check. Done when: Copying a large file into the watched folder produces exactly one settled event, after the copy completes, verified by comparing the observed size to the final size. Creating fifty files at once produces fifty events without dropping any. Restarting the application resumes watching without reprocessing existing files. Do not build yet: No rules, no actions. Reliable observation only. ### Phase 2 · Rules as readable data Build: Conditions on name, extension, size, dates, and content type. Actions to move, rename, tag, copy or run a script. Store both as data. Evaluate rules in priority order with an explicit stop-on-match, and build a dry run that reports every match and intended action without touching anything. Trap: A rule engine that executes as it evaluates cannot offer a dry run, and a file automation tool without a dry run will eventually move a thousand files somewhere you did not intend. Separate matching from acting from the beginning; retrofitting that separation means rewriting the engine. Done when: A dry run over a folder of two hundred files reports intended actions and changes nothing, verified by checksumming the folder before and after. Rules evaluate in priority order and stop-on-match prevents later rules firing. A rule with an impossible condition matches nothing rather than everything. Do not build yet: No actions actually run yet. ### Phase 3 · Acting safely, with undo Build: Perform actions through file coordination so other applications see a consistent state. Record every action with enough detail to reverse it. Refuse to overwrite an existing file, and quarantine anything ambiguous rather than guessing. Trap: Moving a file onto an existing path silently destroys the destination in most naive implementations. Refuse the move, record it as a conflict, and let the user decide. A file automation tool that overwrites will eventually overwrite something irreplaceable. Done when: Running a rule that moves files produces the expected result and a log entry per file. Undoing a run returns every file to its original location, verified by checksum and path. A move onto an existing path is refused and quarantined rather than overwriting. A rule that fails partway leaves a log showing exactly what completed. Do not build yet: No scripts and no scheduling yet. ### Phase 4 · Scripts, and the blast radius they bring Build: A script action passing the matched file path as an argument. Run off the main thread with a timeout and kill the process group on expiry. Cap how many files one rule may act on in a single run, and require confirmation above it. Trap: A rule that matches more broadly than you intended, combined with a script action, is how a folder gets reorganised beyond recognition in seconds. A per-run action cap that requires confirmation above a threshold turns a catastrophe into a question. Done when: A script action receives the file path and its output appears in the log. A script that hangs is killed at its timeout and the rule continues with the next file. A rule matching more files than the cap pauses and asks rather than proceeding. Script actions are visibly marked in the rule list. Do not build yet: No scheduling or import yet. ### Phase 5 · Sharing and inspecting rules Build: Export rules as versioned JSON. Import with validation and a preview that lists every action, with script actions called out individually and approved separately. Show a run history with filtering so you can answer what happened to a file. Trap: An imported rule set containing a script action is arbitrary code from whoever wrote it, running against your files on a trigger you may not fully understand. List every script prominently in the import preview and require explicit approval for each. Done when: Exported rules import into a fresh installation and behave identically. An import preview lists every script action separately and each requires approval. Searching the run history for a filename shows every action taken on it with timestamps. Do not build yet: Signing and distribution come last. ### Phase 6 · Signing, notarization and updates Build: Hardened runtime, Developer ID signing, notarization, stapling and Sparkle updates. Document the blast radius honestly: this application moves and deletes files based on rules, and that deserves a plain statement rather than a feature list. Trap: Full Disk Access is required to watch some locations and is a serious permission. Ask for it only when a rule actually needs it, and explain which folder required it, rather than requesting it at first launch for convenience. Done when: A notarized build opens on a fresh Mac with no Gatekeeper warning. Permissions are requested only when a rule requires them, naming the folder. The README states plainly that the application moves and deletes files and how undo works. Do not build yet: Nothing after this. ### Out of scope (and why) - The rule depth of the commercial product, which has accumulated conditions and actions for a decade of specific needs. - Deep Apple integrations such as Photos and Mail rules, each of which is its own framework and its own permission. - Compatibility across major macOS releases without maintenance, since file coordination and permissions both shift. - Cloud sync of rules, which would mean accounts and a server for a settings file. ### README must contain - That the application moves and deletes files, and that dry run and undo are the safety net - How the settle check works and why a slow copy can still fool it - That a move onto an existing path is refused rather than overwriting - The per-run action cap and how to change it - That imported rule sets can contain scripts and each is approved individually
# Agent instructions · Hazel indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Swift 6 and SwiftUI in Xcode, A background agent with LSUIElement true, FSEvents with debounce and a settle check, Rules and a run log in SQLite under Application Support, Recorded inverse operations, not a trash can. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - A file being copied into a watched folder generates events while it is still incomplete, and acting on it moves or renames a partial file. Wait for the size to be stable across two checks a second or so apart before treating a file as ready, and be aware that a very slow copy can still fool a naive settle check. - A rule engine that executes as it evaluates cannot offer a dry run, and a file automation tool without a dry run will eventually move a thousand files somewhere you did not intend. Separate matching from acting from the beginning; retrofitting that separation means rewriting the engine. - Moving a file onto an existing path silently destroys the destination in most naive implementations. Refuse the move, record it as a conflict, and let the user decide. A file automation tool that overwrites will eventually overwrite something irreplaceable. - A rule that matches more broadly than you intended, combined with a script action, is how a folder gets reorganised beyond recognition in seconds. A per-run action cap that requires confirmation above a threshold turns a catastrophe into a question. - An imported rule set containing a script action is arbitrary code from whoever wrote it, running against your files on a trigger you may not fully understand. List every script prominently in the import preview and require explicit approval for each.
# Build plan · Hazel A background agent watching folders you choose, matching files against rules you can read, and moving, renaming, tagging or running a script on them, with a dry run that shows exactly what would happen and a log that lets you undo anything it did. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Watching a folder without hurting yourself You are told about new and changed files, once each, after they have finished being written. ### Steps 1. Set up the FSEvents stream One stream per watched folder, with a latency that batches bursts. Persist the event identifier so a restart does not replay history. Files: `Watcher.swift` 2. Wait for files to settle Size and modification time stable across two checks. Test with a deliberately slow copy. Files: `Settle.swift` 3. Persist folder access properly Security scoped bookmarks so access to a user-chosen folder survives a relaunch without asking again. Files: `Bookmarks.swift` ### Done when - [ ] A large copy produces one event after it completes - [ ] Fifty simultaneous files produce fifty events - [ ] A restart does not reprocess existing files - [ ] Folder access survives a relaunch without re-prompting ### Watch out - A file being copied into a watched folder generates events while it is still incomplete, and acting on it moves or renames a partial file. Wait for the size to be stable across two checks a second or so apart before treating a file as ready, and be aware that a very slow copy can still fool a naive settle check. ## Phase 2 · Rules as readable data A rule is something you can read, and evaluating it does nothing until you say so. ### Steps 1. Model conditions and actions as data Serialisable structures, not closures. This is what makes dry run and inspection possible. Files: `Rule.swift` 2. Separate match from act One function decides matches; another performs actions. The dry run calls only the first. Files: `Engine.swift` 3. Verify the dry run changes nothing Checksum every file in the folder before and after a dry run. This test is worth more than it looks. Files: `Tests/DryRun.swift` ### Done when - [ ] A dry run reports actions and changes no file, proved by checksums - [ ] Priority order and stop-on-match behave as configured - [ ] An impossible condition matches nothing - [ ] Rules are readable as data in the interface ### Watch out - A rule engine that executes as it evaluates cannot offer a dry run, and a file automation tool without a dry run will eventually move a thousand files somewhere you did not intend. Separate matching from acting from the beginning; retrofitting that separation means rewriting the engine. ## Phase 3 · Acting safely, with undo Rules do their work, and anything they did can be reversed. ### Steps 1. Use file coordination NSFileCoordinator for moves and writes so applications reading the same files see consistency. Files: `Actions.swift` 2. Record reversible operations From path and to path for every action. Undo replays them backwards. Files: `Undo.swift` 3. Refuse rather than overwrite A destination that exists is a conflict, not a decision to make silently. Files: `Actions.swift` ### Done when - [ ] Undo returns every file to its original location - [ ] A move onto an existing path is refused and quarantined - [ ] Each action produces a log entry with both paths - [ ] A partial failure leaves an accurate log of what completed ### Watch out - Moving a file onto an existing path silently destroys the destination in most naive implementations. Refuse the move, record it as a conflict, and let the user decide. A file automation tool that overwrites will eventually overwrite something irreplaceable. ## Phase 4 · Scripts, and the blast radius they bring A rule can run your own script, and a bad script cannot run away with your files. ### Steps 1. Run scripts with a hard timeout Off the main thread, process group killed on expiry, output captured into the log. Files: `ScriptAction.swift` 2. Cap actions per run A threshold above which the run pauses for confirmation. This is the safety net that matters most. Files: `Engine.swift` 3. Mark script rules visibly In the rule list and in the dry run. A script action deserves more attention than a move. Files: `RuleList.swift` ### Done when - [ ] A script receives the file path and its output is logged - [ ] A hanging script is killed and the run continues - [ ] Exceeding the action cap pauses for confirmation - [ ] Script rules are visually distinct in the list ### Watch out - A rule that matches more broadly than you intended, combined with a script action, is how a folder gets reorganised beyond recognition in seconds. A per-run action cap that requires confirmation above a threshold turns a catastrophe into a question. ## Phase 5 · Sharing and inspecting rules Rules can be exported, read and imported without importing a surprise. ### Steps 1. Version the export format A schema version, refused if newer than the application understands. Files: `Export.swift` 2. Preview and approve imports Every action listed, scripts individually approved, and the import applied atomically or not at all. Files: `ImportPreview.swift` 3. Make history searchable By filename, rule and date. Answering what happened to a file is the log's main job. Files: `History.swift` ### Done when - [ ] Exported rules import and behave identically - [ ] Script actions require individual approval on import - [ ] History search by filename shows every action on it - [ ] A failed import leaves existing rules untouched ### Watch out - An imported rule set containing a script action is arbitrary code from whoever wrote it, running against your files on a trigger you may not fully understand. List every script prominently in the import preview and require explicit approval for each. ## Not in this build - The rule depth accumulated by the commercial product. - Deep Photos and Mail integrations. - Guaranteed compatibility across macOS releases. - Cloud sync of rules. ## After v1, if you want it - Content-based conditions such as matching text inside a PDF - A scheduled sweep for folders where watching is impractical - Rule templates for common cases such as sorting downloads by type
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Rules, folders and the run log. RULES_DB_PATH=~/Library/Application Support/Rules/rules.db # Required. How long between size checks before a file counts as settled. Longer is safer for slow copies. SETTLE_CHECK_INTERVAL_MS=1000 # Required. Above this a run pauses for confirmation. The main protection against an over-broad rule. MAX_ACTIONS_PER_RUN=50 # Required. How long a script action may run before it is killed. SCRIPT_TIMEOUT_MS=10000 # Required. How long run history is kept. Undo depends on it, so do not set this short. LOG_RETENTION_DAYS=90
# Hazel · product brief ## Problem The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Hazel, watch selected folders and apply transparent file rules with previews and logs. The hard boundary is mature filesystem automation, rule depth, apple integrations, and reliability, plus os integration polish and maintenance. ## Product outcome A file automation tool whose dry run is trustworthy, whose actions are reversible, which refuses to overwrite, which pauses before doing something at scale, and whose log answers what happened to any file. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - macOS 14 or later - Xcode - only the permissions required by the selected feature ## Explicit non-goals for v1 - The rule depth accumulated by the commercial product. - Deep Photos and Mail integrations. - Guaranteed compatibility across macOS releases. - Cloud sync of rules. - mature filesystem automation, rule depth, Apple integrations, and reliability - years of OS-version edge cases - large extension ecosystem - cross-device sync - commercial support and rapid compatibility fixes ## Success criteria - The dry run checksum test confirms nothing changes - Undo is verified to restore every file from a real run - A move onto an existing path is confirmed refused - The action cap is confirmed to pause on an over-broad rule - Permissions are requested per rule rather than at launch
# Build brief · Hazel The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a macOS folder watcher that applies file rules with a preview and a log of everything it did to replace Hazel. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Language: Swift 6 and SwiftUI in Xcode · FSEvents, file coordination and tags are all platform APIs - App type: A background agent with LSUIElement true · it watches folders; a Dock icon would be noise - Watching: FSEvents with debounce and a settle check · the supported way to hear about changes without polling a disk - Storage: Rules and a run log in SQLite under Application Support · the log is what makes undo possible and it needs to be queryable - Undo: Recorded inverse operations, not a trash can · knowing a file moved from A to B is what lets you put it back ### Data model (create this before Phase 1) - folders(id, path, recursive, enabled, bookmark_data) · bookmark_data is a security scoped bookmark so access survives a relaunch - rules(id, folder_id, name, priority, enabled, conditions_json, actions_json, stop_on_match) - runs(id, rule_id, file_path, matched_at, action, from_path, to_path, result, error) · the log and the undo record - quarantine(path, reason, at) · files a rule refused to touch, with why - A rule is conditions and actions as data, never code, so a dry run can evaluate it without doing anything. ### Phase 1 · Watching a folder without hurting yourself Build: Watch a folder with FSEvents. Debounce events, then wait for the file size and modification time to stop changing before considering it settled. Log what you observed and do nothing else. Trap: A file being copied into a watched folder generates events while it is still incomplete, and acting on it moves or renames a partial file. Wait for the size to be stable across two checks a second or so apart before treating a file as ready, and be aware that a very slow copy can still fool a naive settle check. Done when: Copying a large file into the watched folder produces exactly one settled event, after the copy completes, verified by comparing the observed size to the final size. Creating fifty files at once produces fifty events without dropping any. Restarting the application resumes watching without reprocessing existing files. Do not build yet: No rules, no actions. Reliable observation only. ### Phase 2 · Rules as readable data Build: Conditions on name, extension, size, dates, and content type. Actions to move, rename, tag, copy or run a script. Store both as data. Evaluate rules in priority order with an explicit stop-on-match, and build a dry run that reports every match and intended action without touching anything. Trap: A rule engine that executes as it evaluates cannot offer a dry run, and a file automation tool without a dry run will eventually move a thousand files somewhere you did not intend. Separate matching from acting from the beginning; retrofitting that separation means rewriting the engine. Done when: A dry run over a folder of two hundred files reports intended actions and changes nothing, verified by checksumming the folder before and after. Rules evaluate in priority order and stop-on-match prevents later rules firing. A rule with an impossible condition matches nothing rather than everything. Do not build yet: No actions actually run yet. ### Phase 3 · Acting safely, with undo Build: Perform actions through file coordination so other applications see a consistent state. Record every action with enough detail to reverse it. Refuse to overwrite an existing file, and quarantine anything ambiguous rather than guessing. Trap: Moving a file onto an existing path silently destroys the destination in most naive implementations. Refuse the move, record it as a conflict, and let the user decide. A file automation tool that overwrites will eventually overwrite something irreplaceable. Done when: Running a rule that moves files produces the expected result and a log entry per file. Undoing a run returns every file to its original location, verified by checksum and path. A move onto an existing path is refused and quarantined rather than overwriting. A rule that fails partway leaves a log showing exactly what completed. Do not build yet: No scripts and no scheduling yet. ### Phase 4 · Scripts, and the blast radius they bring Build: A script action passing the matched file path as an argument. Run off the main thread with a timeout and kill the process group on expiry. Cap how many files one rule may act on in a single run, and require confirmation above it. Trap: A rule that matches more broadly than you intended, combined with a script action, is how a folder gets reorganised beyond recognition in seconds. A per-run action cap that requires confirmation above a threshold turns a catastrophe into a question. Done when: A script action receives the file path and its output appears in the log. A script that hangs is killed at its timeout and the rule continues with the next file. A rule matching more files than the cap pauses and asks rather than proceeding. Script actions are visibly marked in the rule list. Do not build yet: No scheduling or import yet. ### Phase 5 · Sharing and inspecting rules Build: Export rules as versioned JSON. Import with validation and a preview that lists every action, with script actions called out individually and approved separately. Show a run history with filtering so you can answer what happened to a file. Trap: An imported rule set containing a script action is arbitrary code from whoever wrote it, running against your files on a trigger you may not fully understand. List every script prominently in the import preview and require explicit approval for each. Done when: Exported rules import into a fresh installation and behave identically. An import preview lists every script action separately and each requires approval. Searching the run history for a filename shows every action taken on it with timestamps. Do not build yet: Signing and distribution come last. ### Phase 6 · Signing, notarization and updates Build: Hardened runtime, Developer ID signing, notarization, stapling and Sparkle updates. Document the blast radius honestly: this application moves and deletes files based on rules, and that deserves a plain statement rather than a feature list. Trap: Full Disk Access is required to watch some locations and is a serious permission. Ask for it only when a rule actually needs it, and explain which folder required it, rather than requesting it at first launch for convenience. Done when: A notarized build opens on a fresh Mac with no Gatekeeper warning. Permissions are requested only when a rule requires them, naming the folder. The README states plainly that the application moves and deletes files and how undo works. Do not build yet: Nothing after this. ### Out of scope (and why) - The rule depth of the commercial product, which has accumulated conditions and actions for a decade of specific needs. - Deep Apple integrations such as Photos and Mail rules, each of which is its own framework and its own permission. - Compatibility across major macOS releases without maintenance, since file coordination and permissions both shift. - Cloud sync of rules, which would mean accounts and a server for a settings file. ### README must contain - That the application moves and deletes files, and that dry run and undo are the safety net - How the settle check works and why a slow copy can still fool it - That a move onto an existing path is refused rather than overwriting - The per-run action cap and how to change it - That imported rule sets can contain scripts and each is approved individually
# Architecture · Hazel ## Stack | Part | Choice | Why | | --- | --- | --- | | Language | Swift 6 and SwiftUI in Xcode | FSEvents, file coordination and tags are all platform APIs | | App type | A background agent with LSUIElement true | it watches folders; a Dock icon would be noise | | Watching | FSEvents with debounce and a settle check | the supported way to hear about changes without polling a disk | | Storage | Rules and a run log in SQLite under Application Support | the log is what makes undo possible and it needs to be queryable | | Undo | Recorded inverse operations, not a trash can | knowing a file moved from A to B is what lets you put it back | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Watcher | Noticing changes and deciding a file has settled. | FSEvents is the platform answer. The settle check is the part that needs care and testing. | | Engine | Matching rules to files. | Pure evaluation over data, testable with no filesystem at all, and the separation from acting is what makes dry run possible. | | Actions | Performing and recording operations. | Each action kind is independent. The refusal to overwrite and the recording of inverses apply to all of them. | | Undo | Reversing recorded operations. | Depends entirely on the log being complete. Any new action kind must record its inverse or undo silently becomes partial. | | Config | Rules, export and import. | JSON so rule sets are readable before they are trusted. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `RULES_DB_PATH` · required · Rules, folders and the run log. - `SETTLE_CHECK_INTERVAL_MS` · required · How long between size checks before a file counts as settled. Longer is safer for slow copies. - `MAX_ACTIONS_PER_RUN` · required · Above this a run pauses for confirmation. The main protection against an over-broad rule. - `SCRIPT_TIMEOUT_MS` · required · How long a script action may run before it is killed. - `LOG_RETENTION_DAYS` · required · How long run history is kept. Undo depends on it, so do not set this short. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Hazel product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Swift 6 and SwiftUI in Xcode, A background agent with LSUIElement true, FSEvents with debounce and a settle check, Rules and a run log in SQLite under Application Support, Recorded inverse operations, not a trash can. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - A file being copied into a watched folder generates events while it is still incomplete, and acting on it moves or renames a partial file. Wait for the size to be stable across two checks a second or so apart before treating a file as ready, and be aware that a very slow copy can still fool a naive settle check. - A rule engine that executes as it evaluates cannot offer a dry run, and a file automation tool without a dry run will eventually move a thousand files somewhere you did not intend. Separate matching from acting from the beginning; retrofitting that separation means rewriting the engine. - Moving a file onto an existing path silently destroys the destination in most naive implementations. Refuse the move, record it as a conflict, and let the user decide. A file automation tool that overwrites will eventually overwrite something irreplaceable. - A rule that matches more broadly than you intended, combined with a script action, is how a folder gets reorganised beyond recognition in seconds. A per-run action cap that requires confirmation above a threshold turns a catastrophe into a question. - An imported rule set containing a script action is arbitrary code from whoever wrote it, running against your files on a trigger you may not fully understand. List every script prominently in the import preview and require explicit approval for each. - Full Disk Access is required to watch some locations and is a serious permission. Ask for it only when a rule actually needs it, and explain which folder required it, rather than requesting it at first launch for convenience.
# Delivery milestones · Hazel Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Watching a folder without hurting yourself You are told about new and changed files, once each, after they have finished being written. ### Steps 1. Set up the FSEvents stream One stream per watched folder, with a latency that batches bursts. Persist the event identifier so a restart does not replay history. Files: `Watcher.swift` 2. Wait for files to settle Size and modification time stable across two checks. Test with a deliberately slow copy. Files: `Settle.swift` 3. Persist folder access properly Security scoped bookmarks so access to a user-chosen folder survives a relaunch without asking again. Files: `Bookmarks.swift` ### Done when - [ ] A large copy produces one event after it completes - [ ] Fifty simultaneous files produce fifty events - [ ] A restart does not reprocess existing files - [ ] Folder access survives a relaunch without re-prompting ### Watch out - A file being copied into a watched folder generates events while it is still incomplete, and acting on it moves or renames a partial file. Wait for the size to be stable across two checks a second or so apart before treating a file as ready, and be aware that a very slow copy can still fool a naive settle check. ## M2 · Rules as readable data A rule is something you can read, and evaluating it does nothing until you say so. ### Steps 1. Model conditions and actions as data Serialisable structures, not closures. This is what makes dry run and inspection possible. Files: `Rule.swift` 2. Separate match from act One function decides matches; another performs actions. The dry run calls only the first. Files: `Engine.swift` 3. Verify the dry run changes nothing Checksum every file in the folder before and after a dry run. This test is worth more than it looks. Files: `Tests/DryRun.swift` ### Done when - [ ] A dry run reports actions and changes no file, proved by checksums - [ ] Priority order and stop-on-match behave as configured - [ ] An impossible condition matches nothing - [ ] Rules are readable as data in the interface ### Watch out - A rule engine that executes as it evaluates cannot offer a dry run, and a file automation tool without a dry run will eventually move a thousand files somewhere you did not intend. Separate matching from acting from the beginning; retrofitting that separation means rewriting the engine. ## M3 · Acting safely, with undo Rules do their work, and anything they did can be reversed. ### Steps 1. Use file coordination NSFileCoordinator for moves and writes so applications reading the same files see consistency. Files: `Actions.swift` 2. Record reversible operations From path and to path for every action. Undo replays them backwards. Files: `Undo.swift` 3. Refuse rather than overwrite A destination that exists is a conflict, not a decision to make silently. Files: `Actions.swift` ### Done when - [ ] Undo returns every file to its original location - [ ] A move onto an existing path is refused and quarantined - [ ] Each action produces a log entry with both paths - [ ] A partial failure leaves an accurate log of what completed ### Watch out - Moving a file onto an existing path silently destroys the destination in most naive implementations. Refuse the move, record it as a conflict, and let the user decide. A file automation tool that overwrites will eventually overwrite something irreplaceable. ## M4 · Scripts, and the blast radius they bring A rule can run your own script, and a bad script cannot run away with your files. ### Steps 1. Run scripts with a hard timeout Off the main thread, process group killed on expiry, output captured into the log. Files: `ScriptAction.swift` 2. Cap actions per run A threshold above which the run pauses for confirmation. This is the safety net that matters most. Files: `Engine.swift` 3. Mark script rules visibly In the rule list and in the dry run. A script action deserves more attention than a move. Files: `RuleList.swift` ### Done when - [ ] A script receives the file path and its output is logged - [ ] A hanging script is killed and the run continues - [ ] Exceeding the action cap pauses for confirmation - [ ] Script rules are visually distinct in the list ### Watch out - A rule that matches more broadly than you intended, combined with a script action, is how a folder gets reorganised beyond recognition in seconds. A per-run action cap that requires confirmation above a threshold turns a catastrophe into a question. ## M5 · Sharing and inspecting rules Rules can be exported, read and imported without importing a surprise. ### Steps 1. Version the export format A schema version, refused if newer than the application understands. Files: `Export.swift` 2. Preview and approve imports Every action listed, scripts individually approved, and the import applied atomically or not at all. Files: `ImportPreview.swift` 3. Make history searchable By filename, rule and date. Answering what happened to a file is the log's main job. Files: `History.swift` ### Done when - [ ] Exported rules import and behave identically - [ ] Script actions require individual approval on import - [ ] History search by filename shows every action on it - [ ] A failed import leaves existing rules untouched ### Watch out - An imported rule set containing a script action is arbitrary code from whoever wrote it, running against your files on a trigger you may not fully understand. List every script prominently in the import preview and require explicit approval for each. ## M6 · Signing, notarization and updates (production only) Others can install it and trust what it will do to their files. ### Steps 1. Sign and notarize Hardened runtime, Developer ID, notarytool, stapler. ```sh xcrun notarytool submit Rules.zip --keychain-profile AC --wait ``` 2. Request permissions lazily Only when a rule needs the location, and say which rule. A first-launch request for everything trains people to click through. Files: `Permissions.swift` 3. State the blast radius One paragraph: this moves and deletes files, here is the dry run, here is undo, here is the cap. Files: `README.md` ### Done when - [ ] spctl reports the notarized build accepted - [ ] Permissions are requested per rule with the folder named - [ ] The README states what the application can do to files - [ ] An appcast update installs and relaunches ### Watch out - Full Disk Access is required to watch some locations and is a serious permission. Ask for it only when a rule actually needs it, and explain which folder required it, rather than requesting it at first launch for convenience.
# Operations · Hazel ## Backup The rules database holds both configuration and the undo log, so losing it loses your ability to reverse recent runs. It is small and belongs in an ordinary backup. ## Restore Replace the database and relaunch. Rules referencing folders that no longer exist should be disabled rather than erroring, so a restore onto a different machine degrades gracefully. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Watch the quarantine count and the rate of refused overwrites. A rising count usually means a rule has become over-broad as the folder's contents changed, which is exactly when it is about to do damage. ## Incident checklist If a rule ran too widely, undo the run from the log first and diagnose afterwards. The action cap should have prevented it; if it did not, that is the bug to fix before re-enabling the rule. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] The dry run checksum test confirms nothing changes - [ ] Undo is verified to restore every file from a real run - [ ] A move onto an existing path is confirmed refused - [ ] The action cap is confirmed to pause on an over-broad rule - [ ] Permissions are requested per rule rather than at launch ## Launch constraint Do not market omitted Hazel capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Rules, folders and the run log. RULES_DB_PATH=~/Library/Application Support/Rules/rules.db # Required. How long between size checks before a file counts as settled. Longer is safer for slow copies. SETTLE_CHECK_INTERVAL_MS=1000 # Required. Above this a run pauses for confirmation. The main protection against an over-broad rule. MAX_ACTIONS_PER_RUN=50 # Required. How long a script action may run before it is killed. SCRIPT_TIMEOUT_MS=10000 # Required. How long run history is kept. Undo depends on it, so do not set this short. LOG_RETENTION_DAYS=90
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for Hazel because small utilities are technically reproducible, but customers pay for polish and immediate fixes when an OS update breaks a daily habit. The recurring cost buys permissions, accessibility APIs, sandboxing, signing, notarization, OS regressions, updates, backups, and support, not just the visible interface.
xmature filesystem automation, rule depth, Apple integrations, and reliability
xyears of OS-version edge cases
xlarge extension ecosystem
xcross-device sync
xcommercial support and rapid compatibility fixes
Don't feel like building it? These folks already made it free.
FForelHazel's folder rules, previews, history, and Finder tags, fresh enough that the paint is still wet.open source↗no votes, no pay-to-list · just what's real
Hazel pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| hazel 6 | custom | — | Single-user perpetual licence for Hazel 6 and all Hazel 6 updates. |
| hazel 6 family pack | custom | — | Up to 5 members of one private household; perpetual Hazel 6 licence. |
| upgrade to hazel 6 | custom | — | Upgrade from any previous Hazel version while retaining the existing single/family licence type. |
free tierno free tier
billingone-time purchase; no monthly or annual plan; trial available
hidden costsFuture major versions can require a paid upgrade, although the purchased Hazel 6 version keeps working. Volume licensing is quoted separately.
verified 2026-08-14 · source ↗
Vibecode Hazel
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Hazel replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Hazel cost?
Hazel's pricing is usage-based or varies by plan · Sold as a one-time licence..
What do I lose by replacing Hazel?
Honestly: mature filesystem automation, rule depth, Apple integrations, and reliability; years of OS-version edge cases; large extension ecosystem; cross-device sync; commercial support and rapid compatibility fixes. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Hazel?
Yes: Forel (Hazel's folder rules, previews, history, and Finder tags, fresh enough that the paint is still wet.) The prompt is for when you want it exactly your way.