File automation is scary. You are giving an app permission to move, rename, or delete your files. One bug and your tax documents are in the trash.
I built AutoShelf with safety as a first principle. Every action can be delayed, confirmed, or both. The system is designed to prevent accidents while staying out of the way for users who trust the app.
The Debounce
The first line of defense is the debounce timer. When a file appears in a watched folder, the engine does not act immediately. It waits 500 milliseconds.
Why? Because files are not always fully written when they first appear. A browser downloading a file creates it immediately and then writes data to it over several seconds. If AutoShelf tried to move the file after the first kernel event, it would catch the file mid-download and move a partial download.
The debounce timer per folder:
debounceTimers[folder]?.cancel()
debounceTimers[folder] = Task {
try? await Task.sleep(nanoseconds: 500_000_000)
await processNewFiles(in: folder)
}
Each new event resets the timer. The folder is only processed after 500 milliseconds of no new events. By then, the file write is complete and the file is ready to be moved.
The Processed Files Set
The second defense is deduplication. When the engine enumerates a folder after a debounce, it finds all the files that are present. It diffs them against a set of already-processed file paths.
private var processedFiles: Set<String> = []
func processNewFiles(in folder: URL) {
guard let contents = try? FileManager.default.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil) else { return }
for fileURL in contents {
let path = fileURL.path
guard !processedFiles.contains(path) else { continue }
processedFiles.insert(path)
evaluateAndExecute(fileURL)
}
// Trim if too large
if processedFiles.count > 10_000 {
processedFiles = Set(processedFiles.suffix(5_000))
}
}
This prevents the same file from being processed multiple times. It also prevents existing files from being processed when the app starts up. On launch, the engine snapshots the current contents of every watched folder and adds them to the processed set before any rules are evaluated.
Delayed Execution
Some rules should not run immediately. The "Auto-delete DMGs" template, for example, moves installer files to the trash after 3 days. This gives the user time to install the software before the installer disappears.
Delayed execution uses Swift's Task.sleep:
func scheduleDelayedExecution(rule: Rule, fileURL: URL) {
let delaySeconds = UInt64(max(rule.delaySeconds, 1)) * 1_000_000_000
let filePath = fileURL.path
let task = Task { [weak self] in
try? await Task.sleep(nanoseconds: delaySeconds)
await self?.executeRuleActions(rule: rule, fileURL: fileURL)
}
pendingDelayed[filePath] = task
}
If the file is deleted or moved before the delay expires, the task is cancelled. The engine keeps a dictionary of pending delayed tasks keyed by file path, so it can cancel them when needed.
The delay is persisted in the rule model as delaySeconds. It is gated behind the Pro purchase because delayed execution is a power feature.
The Confirmation System
For users who want an extra layer of safety, AutoShelf supports confirmation dialogs. When a rule has requiresConfirmation: true, the engine does not execute the action immediately. Instead, it creates a pending confirmation and notifies the user.
The confirmation flows through UNUserNotificationCenter. A banner appears in the notification center with Allow, Deny, and Ignore buttons.
func requestConfirmation(rule: Rule, fileURL: URL) {
let confirmation = PendingConfirmation(
id: UUID(),
ruleID: rule.id,
ruleName: rule.name,
fileName: fileURL.lastPathComponent,
sourcePath: fileURL.path,
actionDescription: rule.actionsDisplaySummary,
timestamp: Date(),
state: .pending,
isMultiple: false
)
activityStore.addConfirmation(confirmation)
showNotification(for: confirmation)
}
If the user taps Allow, the action executes. If they tap Deny, the confirmation is marked as denied and the file is left alone. If they tap Ignore (or do nothing), the confirmation stays in the review list.
The Review List
Confirmations that are not immediately acted on go into a review list. The user can open the Activity tab and see all pending confirmations. They can approve or deny each one, or approve all at once.
This is useful for users who want to review what the app is doing but do not want to sit at their computer watching every file appear.
Multiple File Confirmations
When the same rule matches multiple files at once (for example, cleaning up a folder of old files), I batch them into a single notification. The notification says "AutoShelf wants to move 5 files" and the user can Allow or Deny the entire batch.
if pendingForThisRule > 1 {
confirmation.isMultiple = true
notificationBody = "AutoShelf wants to \(actionDescription) \(pendingForThisRule) files"
}
This prevents notification spam when a rule matches a large number of files at once.
How It All Fits Together
Here is the full flow when a new file appears:
- FileWatcher detects a write event on a watched folder
- The event is debounced for 500ms
- On debounce fire, the engine enumerates the folder
- New files (not in the processed set) are evaluated against enabled rules
- If a rule matches:
a. If delay > 0, schedule delayed execution and return
b. If requiresConfirmation, create a pending confirmation, show a notification, and return
c. Otherwise, execute the action immediately - The action is logged to the activity store
- The file path is added to the processed set
Every step has a guard. Debounce prevents partial files. Dedup prevents re-processing. Delay gives time to undo. Confirmation gives explicit user approval.
Safe Defaults
When I first shipped the confirmation system, I considered making it mandatory for all rules. I decided against it. Power users who set up AutoShelf and trust it should not have to click Allow for every file they download.
The default for new rules is no delay and no confirmation. Users opt in to safety features when they want them. But the templates that ship with the app (like "Auto-delete DMGs" and "Auto-trash ZIPs") use sensible defaults with delays, so new users see the safety features in action.
The Pro gating on confirmation and delay features might seem counterintuitive. Why gate safety features behind a paywall? Because these are power features for users who have complex automation needs. The free tier gives one immediate rule, and that is enough to evaluate whether the app works for you.