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.

17 .skill.md files in the bundleSHA-256 drift detectionAuto-installed on first launchNo npm, no git clone
M
Matthew Diakonov
11 min read
4.7from Sourced from /Users/matthewdi/fazm/Desktop/Sources/SkillInstaller.swift, FazmApp.swift, CHANGELOG.json v2.1.2
Seventeen .skill.md files baked into the signed app
SHA-256 digest compare on every launch, atomic replace on drift
obsoleteSkills array prunes retired skills the same way

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.

Auto-sync

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.

ai-browser-profile · personalgoogle-workspace-setup · productivitytelegram · productivitypdf · documentsdocx · documentsxlsx · documentspptx · documentsvideo-edit · creationfrontend-design · creationcanvas-design · creationdoc-coauthoring · creationdeep-research · researchtravel-planner · researchweb-scraping · researchsocial-autoposter · socialsocial-autoposter-setup · socialfind-skills · discovery

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

Bundle.resourceBundle
FazmApp.swift:326
CHANGELOG.json
SHA-256 compare loop
~/.claude/skills/<name>/SKILL.md
ToastManager.shared.show()
obsoleteSkills wipe

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.

SkillInstaller.swift

What the installer numbers actually look like

Pulled from the v2.4.2 release on April 26, 2026.

0.skill.md files bundled in v2.4.2
0onboarding categories at line 13 to 22
0-bitdigest used for drift detection
0obsolete skill currently being wiped

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

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

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

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

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

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

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/.

FeatureHand-rolled SKILL.mdFazm bundled skills
Where the skill lives on diskUser 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 ownerRead a docs page, copy-paste, restart Claude Code, hope the syntax is rightDrag the .dmg to Applications, sign in, the installer copies all seventeen on first launch
What happens when the skill is updated upstreamManual git pull or re-paste, no notificationSparkle ships a new app build, SkillInstaller diffs SHA-256, toast fires on changed skills
How the skill operates a Mac app like Numbers or MailAppleScript, Automator, or a hand-rolled MCP server the user wires upBundled mcp-server-macos-use binary reads the live accessibility tree, no script writing
Trust posture for shell-style skillsUser decides whether to trust the markdown they pastedSkills are notarized inside the same code-signed app payload, Gatekeeper-aware
Pruning a retired skillDelete the directory by hand or it lingers foreverobsoleteSkills 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.mdCategoryWhat it does
ai-browser-profilePersonalReads your real Chrome profile so the agent has your accounts, preferences, autofill
google-workspace-setupProductivityWalks through Google Cloud OAuth without copy-pasting credentials between three tabs
telegramProductivitySends and reads Telegram messages from a CLI hook the model can call directly
pdfDocumentsReads, splits, merges, and extracts text and tables from local PDF files
docxDocumentsCreates, reads, and edits Word .docx files including frontmatter and tables
xlsxDocumentsOpens, reads, edits, and fixes spreadsheet files (.xlsx, .xlsm, .csv, .tsv)
pptxDocumentsCreates and reads slide decks, including pitch decks and presentations
video-editCreationEdits long videos into short story-driven clips using transcript analysis and ffmpeg
frontend-designCreationProduces production-grade UI designs with design tokens and accessibility checks
canvas-designCreationCreates posters and static visual art as .png and .pdf documents
doc-coauthoringCreationGuides a structured workflow for technical specs, proposals, and decision docs
deep-researchResearchMulti-source synthesis with citation tracking and verification, 10+ sources
travel-plannerResearchTrips, itineraries, budgets, destination advice with one-time profile collection
web-scrapingResearchWeb scraping and data extraction with Python tools
social-autoposterSocialPosts to Reddit, X, LinkedIn, Moltbook with engagement tracking (npm-updated)
social-autoposter-setupSocialFirst-run wizard that creates the database and verifies platform logins
find-skillsDiscoverySearches 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.

Verify ~/.claude/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.