claude/skills/mac-use/SKILL.md
macOS automation using AppleScript and JavaScript for Automation (JXA). Use when automating macOS apps, controlling system features, scripting applications like Music, Notes, Finder, Safari, or any scriptable app. Use when the user asks about currently opened screens, UI elements, or visible content in any macOS app.
npx skillsauth add nounder/dotfiles mac-useInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use JavaScript for Automation (JXA) via osascript -l JavaScript for macOS automation. JXA is preferred over AppleScript for its familiar syntax and better data handling.
sdef /Applications/AppName.app
sdef /System/Applications/Music.app
osascript -l JavaScript -e 'Application("AppName").properties()'
osascript -l JavaScript -e '
const app = Application("AppName")
app.includeStandardAdditions = true
// ... operations
'
osascript -l JavaScript -e '
const finder = Application("Finder")
const selection = finder.selection()
selection.map(f => f.name())
'
// Read file
const app = Application.currentApplication()
app.includeStandardAdditions = true
app.read(Path("/path/to/file"))
// Write file
app.write("content", {to: Path("/path/to/file")})
// Open file for access
app.openForAccess(Path("/path"))
osascript -l JavaScript -e '
const finder = Application("Finder")
// Get selected files
const selection = finder.selection()
selection.map(f => ({name: f.name(), path: f.url()}))
// Create folder
finder.make({new: "folder", at: finder.desktop, withProperties: {name: "New Folder"}})
// Move file
finder.move(finder.files["test.txt"], {to: finder.folders["Documents"]})
'
osascript -l JavaScript -e '
const music = Application("Music")
// Search library
const lib = music.playlists.byName("Library")
const results = lib.search({for: "love"})
results.map(t => ({
name: t.name(),
artist: t.artist(),
album: t.album(),
duration: t.duration()
}))
// Playback control
music.play()
music.pause()
music.nextTrack()
music.previousTrack()
// Current track
const track = music.currentTrack
({name: track.name(), artist: track.artist()})
'
Use System Events to inspect the current UI state and visible content:
osascript -l JavaScript -e '
const se = Application("System Events");
const music = se.processes["Music"];
// Get main content title (artist/album/playlist name being viewed)
const mainContent = music.windows[0].splitterGroups[0].scrollAreas[1].lists[0].lists[0].uiElements[0].staticTexts[0];
mainContent.value();
'
osascript -l JavaScript -e '
const se = Application("System Events");
const music = se.processes["Music"];
// Get Singles & EPs list (list index 4 in main content)
const singlesEPsList = music.windows[0].splitterGroups[0].scrollAreas[1].lists[0].lists[4];
const groups = singlesEPsList.groups();
const albums = [];
for (const group of groups.slice(1)) {
try {
const btns = group.buttons();
if (btns.length > 0) {
const name = btns[0].name();
if (name && name !== "1") {
albums.push(name);
}
}
} catch (e) {}
}
albums.join("\n");
'
osascript -l JavaScript -e '
const se = Application("System Events");
const music = se.processes["Music"];
music.windows[0].entireContents();
'
osascript -l JavaScript -e '
const music = Application("Music")
// Search for artist
const lib = music.playlists.byName("Library")
const results = lib.search({for: "Lab'\''s Cloud"})
// Get unique artists from results
const artists = [...new Set(results.map(t => t.artist()))]
// Find the exact artist match
const targetArtist = artists.find(a => a === "Lab'\''s Cloud")
if (targetArtist) {
// Filter tracks by the selected artist
const artistTracks = results.filter(t => t.artist() === targetArtist)
if (artistTracks.length > 0) {
// Play first track and queue the rest
music.play(artistTracks[0])
// Queue remaining tracks
for (let i = 1; i < artistTracks.length; i++) {
music.playPlaylist(lib, false)
}
"Now playing " + targetArtist + " - queued " + artistTracks.length + " tracks"
}
} else {
"Artist not found"
}
'
osascript -l JavaScript -e '
const notes = Application("Notes")
// Create note
notes.make({new: "note", withProperties: {body: "Hello World"}})
// List folders
notes.folders().map(f => f.name())
// Create note in specific folder
const folder = notes.folders.byName("Notes")
notes.make({new: "note", at: folder, withProperties: {name: "Title", body: "Content"}})
'
osascript -l JavaScript -e '
const safari = Application("Safari")
// Get current URL
safari.windows[0].currentTab.url()
// Get all tab URLs
safari.windows[0].tabs().map(t => ({name: t.name(), url: t.url()}))
// Open URL
safari.openLocation("https://example.com")
// Find existing tab or open new one
const Safari = Application("Safari")
Safari.activate()
let foundHackerNews = false
const frontmostWindow = Safari.windows[0]
// Iterate over all tabs in the frontmost window
for (let i = 0; i < frontmostWindow.tabs.length; i++) {
const tab = frontmostWindow.tabs[i]
const tabUrl = tab.url()
if (tabUrl && tabUrl.includes("news.ycombinator.com")) {
frontmostWindow.currentTab = tab
foundHackerNews = true
break
}
}
// If Hacker News was not found, open it in a new tab
if (!foundHackerNews) {
const newTab = Safari.Tab({url: "https://news.ycombinator.com"})
frontmostWindow.tabs.push(newTab)
frontmostWindow.currentTab = newTab
}
// Execute JavaScript in page
safari.doJavaScript("document.title", {in: safari.windows[0].currentTab})
'
osascript -l JavaScript -e '
const se = Application("System Events")
// List running apps
se.processes().map(p => p.name())
// Click menu item
const proc = se.processes.byName("Finder")
proc.menuBars[0].menuBarItems.byName("File").menus[0].menuItems.byName("New Finder Window").click()
// Keystroke
se.keystroke("v", {using: "command down"})
se.keyCode(36) // Return key
'
osascript -l JavaScript -e '
const app = Application.currentApplication()
app.includeStandardAdditions = true
app.displayNotification("Body text", {
withTitle: "Title",
subtitle: "Subtitle",
soundName: "Frog"
})
'
osascript -l JavaScript -e '
const app = Application.currentApplication()
app.includeStandardAdditions = true
// Alert
app.displayAlert("Title", {message: "Details", buttons: ["OK", "Cancel"]})
// Choose file
app.chooseFile({withPrompt: "Select a file"})
// Choose folder
app.chooseFolder({withPrompt: "Select folder"})
// Text input
app.displayDialog("Enter name:", {defaultAnswer: ""})
'
osascript -l JavaScript -e '
const app = Application.currentApplication()
app.includeStandardAdditions = true
// Get clipboard
app.theClipboard()
// Set clipboard
app.setTheClipboardTo("New content")
'
osascript -l JavaScript -e '
const cal = Application("Calendar")
// List calendars
cal.calendars().map(c => c.name())
// Create event
const calendar = cal.calendars.byName("Home")
cal.Event({
summary: "Meeting",
startDate: new Date("2024-01-15T10:00:00"),
endDate: new Date("2024-01-15T11:00:00")
}).make({at: calendar})
'
osascript -l JavaScript -e '
const rem = Application("Reminders")
// List reminder lists
rem.lists().map(l => l.name())
// Create reminder
const list = rem.lists.byName("Reminders")
rem.Reminder({name: "Buy groceries", dueDate: new Date()}).make({at: list})
'
sdef /path/to/app.app to see available commandsapp.includeStandardAdditions = true for dialogs, file ops.map() to extract valuesconsole.log() and run with osascript -l JavaScript script.jsosascript -l JavaScript -e '
try {
const music = Application("Music")
music.currentTrack.name()
} catch(e) {
"No track playing"
}
'
development
Use when writing Zig code. Contains Zig 0.15 API changes and patterns.
devops
Generate a game-ready 3D asset by running the local AI pipeline sequentially: Fooocus (SDXL text-to-image) -> Hunyuan3D-2 (image-to-textured-GLB) -> optional Blender FBX convert + Unreal import. Use when the user wants to create/generate a 3D model, mesh, or racer for the Unreal train-racer project from a text prompt or a photo. Covers launching both local Gradio services, generating, and wiring the result into Unreal.
testing
Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me".
development
Guides working with exe.dev VMs. Use when the user mentions exe.dev, exe VMs, *.exe.xyz, or tasks involving exe.dev infrastructure.