Inside the Fazm skill installer
Claude skills for Mac automation, shipped inside a signed .app and SHA-256 synced on every launch
Most pages on this topic assume the reader is a developer who will hand-write a SKILL.md, drop it into ~/.claude/skills/, and restart Claude Code. That works for engineers, and it is a non-starter for the Mac owner who wants their agent to clean a spreadsheet, draft a Word invoice, or queue Mail replies. Fazm is the consumer path: seventeen pre-built .skill.md files baked into the signed app, an installer that compares SHA-256 digests on every launch, an obsolete-skill list that prunes retired ones, and a Swift binary that drives any Mac app via the live accessibility tree.
The split most other guides miss
When someone searches the phrase Claude skills for Mac automation, they are usually one of two readers. The first is a developer who already runs Claude Code, has read the official skill spec, and wants tips on writing a SKILL.md that calls a shell tool. The second is a Mac owner who heard that Claude can drive desktop apps and wants the result, not the writing exercise. Almost every existing playbook covers the first case and stops there.
Fazm is built for the second. The skills already exist, the delivery path is a notarized .dmg, and the agent operates the apps on the dock through a Swift binary that reads the live macOS accessibility tree. The rest of this page is what that actually looks like in code: which files ship, which checksums fire, which folders the installer touches.
“The bundled skills now update automatically when the app ships improvements, a brief toast notification appears when any skill is refreshed.”
Fazm CHANGELOG.json, v2.1.2 release notes, April 7, 2026
The seventeen skills, marching past
Each chip below is a real .skill.md file at/Users/matthewdi/fazm/Desktop/Sources/BundledSkills/and shows its onboarding category from SkillInstaller.swift line 13. None of them require a vendor account on first run.
How a single Mac launch refreshes every bundled skill
What flows through the installer between the moment the .app opens and the moment the agent is ready.
SkillInstaller.install() on launch
None of those three touch points require user input. The installer is idempotent, the toast is silent on a clean launch, and the obsolete list is the only deletion path.
The actual code that runs on every launch
Trimmed for readability, but every line that matters is here. The full file is 319 lines at /Users/matthewdi/fazm/Desktop/Sources/SkillInstaller.swift in the project repo.
What the installer numbers actually look like
Pulled from the v2.4.2 release on April 26, 2026.
The 17 in the first cell is a count of files matching*.skill.mdin BundledSkills/. The 7 categories are the keys of categoryOrder at SkillInstaller.swift line 25. The 1 obsolete skill is hindsight-memory, retired when the model context grew large enough that maintaining a separate on-device episodic memory became the wrong tradeoff.
Six steps from app launch to ready agent
What the installer does between the dock-bounce and the moment the floating bar accepts a query. None of it is on the UI thread.
1. App launches and reads the bundled directory
FazmApp.swift line 324 checks UserDefaults.standard.bool(forKey: "hasCompletedOnboarding"). If true, it dispatches SkillInstaller.install() onto a utility-priority background queue so the UI thread is never blocked by file IO.
2. Auto-discovery finds every .skill.md inside the bundle
bundledSkillNames (lines 32 to 43) reads Bundle.resourceBundle.resourceURL/BundledSkills, filters to URLs where pathExtension is md and deletingPathExtension().pathExtension is skill, sorts alphabetically. Adding a new skill is exactly: drop the file, ship the next build.
3. SHA-256 digest of bundled vs installed
sha256(of:) at line 60 hashes both files via CryptoKit's SHA256.hash and joins the digest to a hex string. Line 117 to 122 short-circuits on equal hashes (skipped) so the installer is a no-op for skills you have not edited and the app has not changed.
4. Atomic replace when the bundled file changed
removeItem then copyItem at lines 126 to 127 swap the file in place. If the destination directory does not exist (a fresh skill never installed before), createDirectory at line 135 creates ~/.claude/skills/<name>/ first, then copies the file.
5. Obsolete skills get deleted, not just unlinked
obsoleteSkills at line 66 is the explicit deletion list. Lines 80 to 86 walk it on every launch and FileManager.default.removeItem each match. The current array contains hindsight-memory, the on-device episodic memory layer Fazm retired in favor of larger context windows.
6. Toast fires only when something actually changed
If updated.isEmpty is false (lines 161 to 169), DispatchQueue.main.async dispatches ToastManager.shared.show("Skill updated: name" or "N skills updated: a, b"). A clean re-launch where nothing diverged shows nothing, which is the desired behavior for a consumer app that auto-updates silently.
Hand-rolled SKILL.md vs Fazm bundled delivery
The two paths diverge at delivery, update, and trust. They converge at runtime, since both produce a SKILL.md inside ~/.claude/skills/.
| Feature | Hand-rolled SKILL.md | Fazm bundled skills |
|---|---|---|
| Where the skill lives on disk | User clones or hand-writes a SKILL.md into ~/.claude/skills/<name>/ | Seventeen .skill.md files baked into the signed .app at Contents/Resources/BundledSkills/ |
| How the skill reaches a non-developer Mac owner | Read a docs page, copy-paste, restart Claude Code, hope the syntax is right | Drag the .dmg to Applications, sign in, the installer copies all seventeen on first launch |
| What happens when the skill is updated upstream | Manual git pull or re-paste, no notification | Sparkle ships a new app build, SkillInstaller diffs SHA-256, toast fires on changed skills |
| How the skill operates a Mac app like Numbers or Mail | AppleScript, Automator, or a hand-rolled MCP server the user wires up | Bundled mcp-server-macos-use binary reads the live accessibility tree, no script writing |
| Trust posture for shell-style skills | User decides whether to trust the markdown they pasted | Skills are notarized inside the same code-signed app payload, Gatekeeper-aware |
| Pruning a retired skill | Delete the directory by hand or it lingers forever | obsoleteSkills array (SkillInstaller.swift line 66) wipes the directory on next launch |
A complete inventory, one skill per row
Every row maps to a real file inside the .app. The category column reflects categoryMap at SkillInstaller.swift line 13.
| .skill.md | Category | What it does |
|---|---|---|
| ai-browser-profile | Personal | Reads your real Chrome profile so the agent has your accounts, preferences, autofill |
| google-workspace-setup | Productivity | Walks through Google Cloud OAuth without copy-pasting credentials between three tabs |
| telegram | Productivity | Sends and reads Telegram messages from a CLI hook the model can call directly |
| Documents | Reads, splits, merges, and extracts text and tables from local PDF files | |
| docx | Documents | Creates, reads, and edits Word .docx files including frontmatter and tables |
| xlsx | Documents | Opens, reads, edits, and fixes spreadsheet files (.xlsx, .xlsm, .csv, .tsv) |
| pptx | Documents | Creates and reads slide decks, including pitch decks and presentations |
| video-edit | Creation | Edits long videos into short story-driven clips using transcript analysis and ffmpeg |
| frontend-design | Creation | Produces production-grade UI designs with design tokens and accessibility checks |
| canvas-design | Creation | Creates posters and static visual art as .png and .pdf documents |
| doc-coauthoring | Creation | Guides a structured workflow for technical specs, proposals, and decision docs |
| deep-research | Research | Multi-source synthesis with citation tracking and verification, 10+ sources |
| travel-planner | Research | Trips, itineraries, budgets, destination advice with one-time profile collection |
| web-scraping | Research | Web scraping and data extraction with Python tools |
| social-autoposter | Social | Posts to Reddit, X, LinkedIn, Moltbook with engagement tracking (npm-updated) |
| social-autoposter-setup | Social | First-run wizard that creates the database and verifies platform logins |
| find-skills | Discovery | Searches skillhu.bz and skills.sh, presents matches, installs the chosen one |
Verify the install on your own Mac
The whole point of a checksum-based install is that you can audit it from the shell. Here is the sequence I run when I want to confirm v2.4.2 actually pushed a new version of one of the skills.
What you need on the Mac for any of this to work
Six preconditions, one per line. The first two are about the file system and the binary; the next three are TCC permissions; the last one is the directory the installer writes to.
Preconditions
- macOS 13+ (the AXUIElement APIs Fazm calls are present back to Ventura)
- Signed Fazm.app from fazm.ai (notarization is what makes Gatekeeper accept the bundled SKILL.md files)
- Accessibility permission granted (App is checking via real AXUIElementCopyAttributeValue, not the cached flag)
- Screen Recording permission for the canvas-app fallback path
- App Management permission on macOS 26 so Sparkle can replace the signed bundle
- ~/.claude/skills/ directory writable (SkillInstaller creates it with createDirectory if missing)
Why a checksum and not a version string
Skills do not carry a version field in their YAML frontmatter, and they probably should not. The contract for a Claude skill is the markdown body and the description. A maintainer who rewrites a clarifying paragraph in the docx skill should not have to bump a number; the body already changed and the checksum will catch it. A maintainer who does nothing should not have to write a release note that nothing changed; the checksum will be identical and the installer will skip.
This also gives a clean answer for the local-edit case. If a user edits ~/.claude/skills/xlsx/SKILL.md, their digest no longer matches the bundled digest. On the next launch with the same .app, the installer overwrites their edit, because the mismatch is indistinguishable from a stale install. The comment at line 5 of SkillInstaller.swift documents this directly: user-edited skills that differ from the bundled version will be overwritten if the bundle was updated. The right fork for a user with strong opinions is to publish their version under a different name so the directory does not collide; find-skills then resolves it the same way it resolves anything else from skillhu.bz or skills.sh.
The npm fork for fast-moving runtime code
One of the seventeen skills, social-autoposter, is updated faster than the app shipping cadence because it includes a TypeScript runner. The npmSkills tuple at SkillInstaller.swift line 177 declares the package, and checkNpmSkillUpdates at line 183 runs npm view, compares the version, and shells npx --yes social-autoposter update if the remote is newer.
The two-track pattern is the cleaner answer than picking one delivery channel. Markdown body of a skill: ship in the .app, checksum-compare. Code that the markdown wraps: ship via npm, version-compare. A reader writing their own skill installer for a Mac app could lift the same split.
See the seventeen bundled skills in action
Fifteen minutes, screen share, the agent runs against your real Mac apps.
Frequently asked questions
What is the actual delivery mechanism for Claude skills inside Fazm on a Mac?
Fazm ships seventeen .skill.md files inside the signed .app at the resource path BundledSkills/. On every launch, after the user has completed onboarding, Fazm runs SkillInstaller.install() from a background utility queue. The function reads each bundled file from Bundle.resourceBundle, walks ~/.claude/skills/<name>/SKILL.md, and compares the SHA-256 digest of both copies. If the bundled hash differs, the installed file is removed and replaced atomically; if it matches, the file is left alone. New skills get a fresh directory created with createDirectory(withIntermediateDirectories: true). The list of names is auto-discovered, not hard coded: the loop filters Bundle.resourceBundle.resourceURL/BundledSkills for files whose pathExtension is md and whose deletingPathExtension().pathExtension is skill, which means a developer adds a skill by dropping the .skill.md file in /Users/matthewdi/fazm/Desktop/Sources/BundledSkills/ and shipping the next build, no code change to the installer required. The relevant code lives at /Users/matthewdi/fazm/Desktop/Sources/SkillInstaller.swift between lines 36 and 172.
Why use a checksum compare instead of just overwriting on every launch or never overwriting?
Two reasons. Always overwriting would clobber a user's local edits to a SKILL.md every single launch, which would be hostile to anyone who customized their workflow. Never overwriting would mean a user who installed Fazm v1.5 and let it auto-update to v2.4.2 would still be running the v1.5 version of every bundled skill, so a skill bug fix in the new app build would never reach them. The SHA-256 compare resolves both. If the user has not changed the file and Fazm has shipped a new version of the skill, the digests differ and the installed file is replaced. If the user has edited the file and Fazm has not changed the bundled version, the digests differ in the same direction and the user's edit is also overwritten, which is the documented tradeoff in the comment at line 5 of SkillInstaller.swift: user-edited skills that differ from the bundled version will be overwritten if the bundle was updated. The honest framing for a Mac owner is: Fazm treats bundled skills like first-party app resources, not user data.
How does Fazm handle a skill that gets retired? Does it leave the old file behind?
No. SkillInstaller.swift line 66 declares a private static let obsoleteSkills: [String] = ["hindsight-memory"]. Lines 80 to 86 walk that list on every launch, check whether ~/.claude/skills/<name> exists, and if so call FileManager.default.removeItem to delete the directory. The log line is SkillInstaller: removed obsolete skill 'name'. The hindsight-memory skill was the on-device episodic memory layer that Fazm shipped with for several months and then retired when the model context windows grew large enough that maintaining a separate memory store was the wrong tradeoff. Adding any skill name to that array deletes it from the user's machine on the next launch, which is the only mechanism Fazm has for actively pruning a stale skill.
What are the seventeen skills, and what category does each fall under?
The full inventory at /Users/matthewdi/fazm/Desktop/Sources/BundledSkills/ as of v2.4.2 (April 26, 2026): ai-browser-profile, canvas-design, deep-research, doc-coauthoring, docx, find-skills, frontend-design, google-workspace-setup, pdf, pptx, social-autoposter, social-autoposter-setup, telegram, travel-planner, video-edit, web-scraping, xlsx. The categoryMap at SkillInstaller.swift lines 13 to 22 groups them into seven onboarding categories. Personal: ai-browser-profile. Productivity: google-workspace-setup, telegram. Documents: pdf, docx, xlsx, pptx. Creation: video-edit, frontend-design, canvas-design, doc-coauthoring. Research & Planning: deep-research, travel-planner, web-scraping. Social Media: social-autoposter, social-autoposter-setup. Discovery: find-skills. The category labels never end up inside the .skill.md frontmatter; they exist only to drive the order of the onboarding list returned by SkillInstaller.listBundledSkills() at line 290.
How does a Claude skill end up clicking a real Mac app rather than just a chat reply?
The skills themselves are markdown instructions, not executables. The clicking happens through a separate Swift binary called mcp-server-macos-use that lives inside the .app bundle at Contents/MacOS/mcp-server-macos-use, registered as a built-in MCP server in /Users/matthewdi/fazm/acp-bridge/src/index.ts between lines 1056 and 1064. When a skill says "open Numbers and filter the unpaid rows," the agent calls a tool exposed by that MCP server. The binary reads the live macOS accessibility tree through AXUIElementCopyAttributeValue, finds the AXRow elements whose AXValue at the Status column reads Unpaid, and calls AXUIElementPerformAction with kAXPressAction to click each one. Two kilobytes of structured text per app window, no PNG round trip unless the app is a canvas like Figma, no Apple Script, no Automator. The accessibility-tree path is what makes the skills operable on apps that have no public API.
Why ship skills inside a signed app instead of telling users to clone a GitHub repo into ~/.claude/skills/?
Three reasons that matter to the kind of person Fazm targets. First, the audience is Mac owners running their business or their personal life, not developers. They will not run git clone, npm install, or chmod +x anything. Second, a signed bundle gives the operating system a trust anchor. The skills inside the .app are notarized along with the binary, so macOS Gatekeeper accepts them as part of the same code-signed payload, and the user is not asked to bypass quarantine on individual files. Third, the auto-update path falls out for free. Sparkle ships a new .dmg, the .app launches, SkillInstaller diffs every bundled skill against the installed copy, atomically replaces the changed ones, and shows a toast. The April 7, 2026 changelog entry for v2.1.2 documents this: Bundled skills now update automatically when the app ships improvements, a brief toast notification appears when any skill is refreshed.
Does Fazm conflict with skills I already have at ~/.claude/skills/ from other tools?
No. The installer only touches directories whose name matches one of the seventeen bundled skill names plus the one entry in obsoleteSkills. If you keep a custom skill at ~/.claude/skills/my-build-script/SKILL.md, Fazm never reads it, never compares it, and never deletes it. Same for skills you installed from other registries like skillhu.bz or skills.sh, as long as the directory name does not collide with a bundled name. The find-skills skill at /Users/matthewdi/fazm/Desktop/Sources/BundledSkills/find-skills.skill.md is exactly the on-ramp for that case: when you ask Fazm for a capability that no bundled skill covers, the agent runs npx skillhu search or npx skills find, presents matches, and offers to install one. That installs into ~/.claude/skills/<name>/ alongside the bundled set, untouched on subsequent launches.
What permissions does a Mac owner have to grant for any of this to work?
Two TCC permissions and one Sparkle-related App Management permission. Accessibility (so AXUIElementCopyAttributeValue can read other apps), Screen Recording (so the fallback path can take a targeted screenshot of canvas-style apps), and App Management for the auto-updater on macOS 26. The first launch checks the permission status with a real AXUIElementCopyAttributeValue call against the frontmost app, not just AXIsProcessTrusted, because that flag can return stale values after macOS updates or a re-sign. The cross-check function lives at /Users/matthewdi/fazm/Desktop/Sources/AppState.swift line 311 inside checkAccessibilityPermission, with a CGEvent.tapCreate fallback probe at line 341 for the macOS 26 Tahoe TCC cache bug. If permission is genuinely missing the app shows the system pane with the right toggle highlighted, not a doc page telling you to open Settings yourself.
Where does the npm-based skill update path fit in?
One bundled skill, social-autoposter, is npm-distributed because its workflow includes a TypeScript runner that we update faster than the app shipping cadence. The npmSkills tuple at SkillInstaller.swift line 177 declares (name: "social-autoposter", package: "social-autoposter", dir: "social-autoposter"). On every launch, after the static .skill.md sync, checkNpmSkillUpdates reads ~/social-autoposter/package.json, runs npm view social-autoposter version, compares the strings, and if the remote is newer runs npx --yes social-autoposter update from the user's home directory. The toast on success reads social-autoposter updated to vX.Y.Z. That two-track pattern (static skills via app bundle, code-heavy skills via npm) is what lets Fazm ship slow-moving skill instructions in the signed app while still letting fast-moving runtime code update independently.
How do I verify any of this on my own Mac?
Open Terminal and ls ~/.claude/skills after Fazm has run once. You will see seventeen directories matching the names above, each containing a SKILL.md. Compare a SHA-256 of one of them against the bundled file inside the .app: shasum -a 256 ~/.claude/skills/xlsx/SKILL.md and shasum -a 256 /Applications/Fazm.app/Contents/Resources/BundledSkills/xlsx.skill.md. If they match, the installer skipped the file on the most recent launch; if they differ, you are mid-edit. Tail Fazm's log at /tmp/fazm.log and grep for SkillInstaller to see the install/update/skip summary on the next launch. The log line format is SkillInstaller: Installed N skills: a, b, c. Updated N skills: d, e. Skipped N (up to date): f, g. The exact format is set at SkillInstaller.swift line 144 to 158.
More on what the bundled skills unlock and why the accessibility-tree path matters.
Adjacent reads
AI agent for small business admin
What the seventeen bundled skills actually let a one-person operation do on day one, mapped to Numbers, Mail, QuickBooks Desktop, and the rest of the dock.
AI agent ask clarifying questions
How a Mac-resident agent decides when to confirm a tool call vs run it, and where the bundled skill descriptions tighten that gate.
Anthropic VAT EU Mac automation impact
Why a per-token model bill on a Mac-native agent is structurally different than on a SaaS dashboard, and how the accessibility-tree path changes the math.