How to Automate Your Mac With Swift in 2026: 4 Scripts to Copy
Write a Swift script on your Mac, run it from Terminal or a Shortcuts hotkey, and schedule it with launchd. Four scripts to copy for files, links and apps.
To automate your Mac with Swift, install Apple's Command Line Tools, save a file that starts with `#!/usr/bin/env swift`, make it executable with `chmod +x`, and run it from Terminal. A script can call Apple's own frameworks, so it can move files, rewrite the clipboard, open and quit apps, and then run from a Shortcuts hotkey or on a schedule.
Swift is a good fit when Shortcuts gets clumsy and a shell one-liner gets unreadable: you want a loop, a date, a bit of logic, and direct access to the same APIs Mac apps use. This guide sets up the tools, walks through four scripts you can copy as they are, then covers running them from a hotkey or on a timetable, and the usual reasons one does nothing.
Best for | Where it falls short | |
|---|---|---|
Shortcuts app | Chaining built-in actions, no code | Loops, string handling and file logic get awkward fast |
Shell script (zsh) | Gluing command-line tools together | Hard to read once it needs dates, URLs or app control |
AppleScript | Telling scriptable apps such as Mail or Finder what to do | An unusual syntax, and each app supports it differently |
Swift script | Files, clipboard, URLs and apps through Apple's frameworks | Needs the developer tools, and starts slower than a shell script unless compiled |
What you need before your first Swift script
A Swift script needs the Swift toolchain, which comes with Xcode or with Apple's smaller Command Line Tools for Xcode package. Apple says the package "contains the same macOS SDK, man (manual) pages, and toolchain binaries that ship with Xcode," and installs to `/Library/Developer/CommandLineTools` (Apple's install guide).
- Open Terminal, which lives in Applications → Utilities (Spotlight finds it too).
- Type `xcode-select --install` and press Return.
- In the dialog that appears, click Install, agree to the license and click Done when it finishes (Apple).
- Type `swift --version` and press Return. If it prints a version number, you're ready.
If you already have Xcode, skip the install; it ships with the same tools. Swift.org also offers a toolchain installer called swiftly for people who want the newest Swift release outside Xcode (Swift.org). For scripts like the ones below, Apple's package is enough. After a big macOS update, check Software Update for a newer Command Line Tools release, since Apple warns the installed version "may be incompatible with the new macOS" (Apple).
How to write and run a Swift script
A Swift script is a plain text file with a `.swift` extension and a first line that tells macOS to run it with Swift. You can run any such file with `swift name.swift`, or add the shebang line `#!/usr/bin/env swift`, mark it executable with `chmod +x` and run it like any other command (a walkthrough of Swift without Xcode).
I keep all of mine in one folder called Scripts in my home folder, so every path in this guide starts with `~/Scripts`. Create it with `mkdir ~/Scripts`, then save this as `~/Scripts/today.swift` in any plain text editor (TextEdit works if you choose Format → Make Plain Text first):
#!/usr/bin/env swift
import Foundation
let formatter = DateFormatter()
formatter.dateStyle = .full
print("Today is \(formatter.string(from: Date())).")Then make it executable and run it:
chmod +x ~/Scripts/today.swift
~/Scripts/today.swiftCode at the top level of a script runs from top to bottom, so there's no `main` function to write. `import Foundation` gives you dates, files and URLs; `import AppKit` adds the clipboard, running apps and opening things, and it includes Foundation too. Running the .swift file directly can take a moment to start. If that delay bothers you, compile it once with `swiftc -O today.swift -o today` and run the `today` file instead; the swiftc command builds a standalone program from the file (walkthrough).
Script 1: strip tracking tags from a copied link
This script reads the link on your clipboard, removes the utm_ campaign parameters and puts the clean link back, ready to paste. The utm_ tags (utm_source, utm_medium, utm_campaign and others) are the ones analytics tools read to credit a visit to a campaign, as Google Analytics' URL builder guide lists them. Whoever reads your email doesn't need them. Save it as ~/Scripts/clean-link.swift and run chmod +x on it, as with the first script.
#!/usr/bin/env swift
import AppKit
let pasteboard = NSPasteboard.general
guard let text = pasteboard.string(forType: .string),
var link = URLComponents(string: text.trimmingCharacters(in: .whitespacesAndNewlines)),
link.scheme != nil else {
print("The clipboard doesn't hold a link.")
exit(1)
}
let kept = link.queryItems?.filter { !$0.name.lowercased().hasPrefix("utm_") }
link.queryItems = (kept?.isEmpty ?? true) ? nil : kept
guard let clean = link.string else { exit(1) }
pasteboard.clearContents()
pasteboard.setString(clean, forType: .string)
print(clean)Two Apple APIs do the work. `NSPasteboard.general` is the system clipboard, and writing to it means calling `clearContents()` before `setString(_:forType:)` (Apple's NSPasteboard reference). `URLComponents` splits a link into parts, so the query items can be filtered by name instead of hacked apart with text searches (Apple). Setting the query items to `nil` when none are left also drops the dangling question mark.
I paste a lot of links into emails and shared docs, and the ones copied from newsletters used to arrive with a tail of tracking tags longer than the address itself. This is the script I run most, usually from a keyboard shortcut set up as shown further down. To strip other parameters too, add their names to the filter, and test it on a few links you use before trusting it with a whole day's work.
Script 2: file screenshots from the desktop by month
Screenshots taken with the keyboard shortcuts land on the desktop with names that begin with "Screenshot" or "Screen Recording" followed by the date and time, per Apple's screenshot guide. This script moves them into a folder for each month inside Pictures, and skips anything already filed under the same name. Save it as ~/Scripts/tidy-screenshots.swift.
#!/usr/bin/env swift
import Foundation
let files = FileManager.default
let home = files.homeDirectoryForCurrentUser
let desktop = home.appendingPathComponent("Desktop")
let archive = home.appendingPathComponent("Pictures/Screenshots")
let month = DateFormatter()
month.locale = Locale(identifier: "en_US_POSIX")
month.dateFormat = "yyyy-MM"
let prefixes = ["Screenshot", "Screen Shot", "Screen Recording"]
let items = try files.contentsOfDirectory(at: desktop,
includingPropertiesForKeys: [.creationDateKey],
options: [.skipsHiddenFiles])
var moved = 0
for item in items where prefixes.contains(where: { item.lastPathComponent.hasPrefix($0) }) {
let created = (try? item.resourceValues(forKeys: [.creationDateKey]))?.creationDate ?? Date()
let folder = archive.appendingPathComponent(month.string(from: created))
try files.createDirectory(at: folder, withIntermediateDirectories: true)
let destination = folder.appendingPathComponent(item.lastPathComponent)
if files.fileExists(atPath: destination.path) { continue }
try files.moveItem(at: item, to: destination)
moved += 1
}
print("Moved \(moved) file(s) to \(archive.path)")Everything here is `FileManager`, Foundation's class for listing, creating and moving files (Apple's FileManager reference). The "Screen Shot" prefix catches files from older versions of macOS that used that spelling. The fixed `en_US_POSIX` locale keeps the folder names as 2026-11 whatever language your Mac is set to.
The first time you run it, macOS will likely ask whether Terminal can access files in your Desktop folder. Desktop, Documents and Downloads are protected, and the answer you give can be changed later under Files & Folders in the Privacy & Security pane of System Settings (Apple). My desktop used to be a wall of screenshots by midweek. Now it's empty most evenings, and when I need last month's capture of a settings page, it's in one folder instead of buried under forty others.
Script 3: close distractions and open what you need
This one quits the apps you don't want during focused work, opens the one you do, and confirms with a notification. Save it as ~/Scripts/focus.swift. Apps are named by bundle ID, which is more reliable than the name in the Dock, and `NSRunningApplication.runningApplications(withBundleIdentifier:)` returns every running copy of an app with that ID (Apple)).
#!/usr/bin/env swift
import AppKit
let quit = ["com.apple.mail", "com.apple.MobileSMS"] // Mail and Messages
for id in quit {
for app in NSRunningApplication.runningApplications(withBundleIdentifier: id) {
app.terminate()
}
}
NSWorkspace.shared.open(URL(fileURLWithPath: "/System/Applications/Notes.app"))
let notify = Process()
notify.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
notify.arguments = ["-e", "display notification \"Mail and Messages are closed.\" with title \"Focus time\""]
try notify.run()
notify.waitUntilExit()Three things in it behave in ways that can surprise you the first time:
- Quitting is polite. `terminate()` asks the app to quit normally, the same as choosing Quit, so an app with unsaved work can still ask you to save. It may return before the app has actually closed (Apple)).
- Opening takes any URL. `NSWorkspace.shared.open(_:)` opens a location and reports whether it worked (Apple)), so you can swap Notes for a document, a folder or a web address such as your team's board.
- The notification comes from AppleScript. `Process` runs another program as a subprocess (Apple); here it runs osascript with AppleScript's `display notification` command, which takes a message and an optional title, subtitle and sound (Apple's Mac Automation Scripting Guide).
To find any app's bundle ID, run `osascript -e 'id of app "Slack"'` in Terminal, with the app's name in place of Slack, and paste what it prints into the list. Before a writing session, I run this to close Mail and Messages, since those are the two that pull me out of a paragraph, and open Notes, where I draft.
Script 4: pass in what the script should work on
Scripts become reusable once they take arguments, which Swift reads from `CommandLine.arguments`. The first item is the script's own path, and everything you type after it follows. This small one creates a dated project folder with the subfolders I want every time. Save it as ~/Scripts/new-project.swift:
#!/usr/bin/env swift
import AppKit
let args = CommandLine.arguments
guard args.count > 1 else {
print("Usage: new-project.swift \"Project name\"")
exit(1)
}
let day = DateFormatter()
day.locale = Locale(identifier: "en_US_POSIX")
day.dateFormat = "yyyy-MM-dd"
let name = "\(day.string(from: Date())) \(args[1])"
let root = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Documents/Projects/\(name)")
for sub in ["Brief", "Drafts", "Final"] {
try FileManager.default.createDirectory(at: root.appendingPathComponent(sub),
withIntermediateDirectories: true)
}
NSWorkspace.shared.open(root)Run it as `~/Scripts/new-project.swift "Spring launch"`, with quotes around any name that has spaces. It imports AppKit rather than Foundation because the last line, which opens the new folder in Finder, uses `NSWorkspace`. Swap the three subfolder names for the ones your own projects always need.
Run Swift scripts without opening Terminal
Terminal is fine while you're testing, but a script earns its keep when it runs from a keystroke or on its own. The Shortcuts app gives you a hotkey, and launchd, the system's own scheduler, gives you a timetable.
From a keyboard shortcut, with the Shortcuts app
- In Shortcuts, choose Shortcuts → Settings, click Advanced and select Allow Running Scripts. Apple warns that "running shortcuts with scripts could cause data loss," so only run scripts you've read (Apple).
- Create a new shortcut, search the actions for Run Shell Script and add it.
- In the script box, type the full path to your script, such as `~/Scripts/clean-link.swift`.
- Double-click the shortcut, click the details button, click Add Keyboard Shortcut and type the combination. Some combinations are reserved by macOS and won't take (Apple).
Shortcuts on the Mac can also run as automations when an event happens, such as a time of day or opening an app. Apple's guide says you get a notification asking you to run the automation when the event occurs (Apple), so it's a nudge rather than a silent run. For a job that should run by itself, use launchd.
On a schedule, with launchd
launchd runs jobs described in small property list files. Per-user jobs, called agents, go in the LaunchAgents folder inside your own Library folder, and each needs a unique `Label` and the `ProgramArguments` to run (Apple's launchd guide). A `StartCalendarInterval` dictionary sets the time, with keys such as Hour and Minute (Apple).
Compile the script first (`swiftc -O ~/Scripts/tidy-screenshots.swift -o ~/Scripts/tidy-screenshots`), because a scheduled job is simpler to run as a plain program. Then save this as `~/Library/LaunchAgents/local.tidy-screenshots.plist`, replacing `you` with your Mac user name, since launchd needs the full path:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>local.tidy-screenshots</string>
<key>ProgramArguments</key>
<array>
<string>/Users/you/Scripts/tidy-screenshots</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>18</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
</dict>
</plist>launchd reads your LaunchAgents folder when you log in (Apple). To load the job now, run `launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/local.tidy-screenshots.plist`, and remove it again with `launchctl bootout` and the same arguments; `bootstrap` and `bootout` replace the older load and unload commands (launchctl reference; `man launchctl` on your Mac has the details for your version).
A scheduled job won't run on a Mac that's turned off
Apple says a `StartCalendarInterval` job that comes due while the Mac is asleep runs when it wakes, but if the Mac is turned off, the job waits until the next scheduled time (Apple). A 1 a.m. job on a laptop that's shut down every night never runs. I set mine for 6 p.m., near the end of my working day.
When a Swift script won't run
A Swift script that fails usually trips on the toolchain, the execute flag, a privacy permission or a stale compiled copy. Match the symptom:
- "zsh: permission denied": the file isn't marked executable. Run `chmod +x` on it.
- "xcrun: error: invalid active developer path" or "command not found: swift": the Command Line Tools are missing or out of date, which can happen after a macOS update. Run `xcode-select --install` again (Apple).
- It runs in Terminal but does nothing in Shortcuts: check that Allow Running Scripts is still on (Apple), and use the full path to the script, not just its name.
- It runs but can't see your files: open the Files & Folders list in System Settings and turn on access for the app that ran it, which is Terminal, Shortcuts or the compiled program itself when launchd runs it (Apple).
- A compiled version ignores your edits: a compiled program doesn't change when you edit the .swift file. Run swiftc again after every change.
Where to take Swift automation next
Once one script works, the next tends to follow the same shape. A few directions that build on the four above:
- Combine them. Call the screenshot tidy-up and the focus script from one "end of day" script with `Process`, so a single keystroke resets your Mac.
- Give a menu command a key instead. If what you want is one command an app already has, a custom shortcut in Keyboard settings may beat a script; our Mac keyboard shortcuts guide covers how.
- Try a launcher. Raycast's Script Commands can run Swift scripts from its search bar, as Raycast's own Swift examples show, and our Raycast alternatives page compares it with Alfred and Spotlight.
- Start from the built-ins. Before writing code, check what macOS already does; these Mac productivity tips cover Spotlight actions, clipboard history and window tiling.
For the jobs a script can't judge
Swift is great at moving files and flipping switches on your Mac. It can't decide which of today's emails matter. Merlin, our Mac app, has Routines for that kind of work: you describe the job in plain words, such as a short brief from Gmail and Calendar before each meeting, then test it and turn it on. An active routine works without asking you each time, so try a test run before you let one post to Slack or send email for you.
Get MerlinFrequently asked questions
Can you use Swift for scripting on a Mac?
Yes. Install Apple's Command Line Tools with `xcode-select --install`, start a file with `#!/usr/bin/env swift`, make it executable with `chmod +x` and run it from Terminal. You can also run any .swift file with `swift name.swift`.
Do I need Xcode to run Swift scripts?
No. Apple's Command Line Tools for Xcode package contains the same toolchain that ships with Xcode, so it's enough for scripts without the full Xcode install (Apple).
Is Swift better than AppleScript for automating a Mac?
For files, the clipboard, URLs and opening or quitting apps, Swift is usually easier to read and test. For telling a scriptable app such as Mail what to do inside its own windows, AppleScript still reaches further, and a Swift script can run AppleScript through osascript when it needs to.
How do I run a Swift script with a keyboard shortcut?
Turn on Allow Running Scripts in Shortcuts settings, add a Run Shell Script action with the path to your script, then open the shortcut's details and click Add Keyboard Shortcut (Apple).
How do I run a Swift script automatically every day on a Mac?
Compile it with swiftc, then add a launchd agent to ~/Library/LaunchAgents with a StartCalendarInterval for the time you want. If the Mac is off at that time, the job waits for the next one (Apple).
Why is my Swift script slow to start?
Running a .swift file directly can take a moment to start each time. Compile it once with swiftc -O and run the resulting program for an instant start, then recompile after each edit.