A native macOS menu bar backup manager powered by restic.
Snapshotter supports local folders, S3-compatible object storage, SFTP, and rest-server destinations. Repository encryption passwords and remote service credentials can be stored in macOS Keychain or a synced 1Password vault. SFTP uses the system SSH agent, configuration, and keys.
app/— React and TypeScript interfacemacos/— native Swift menu bar host and system integrationengine/— Go backup-domain service and restic integration boundary
Install dependencies once:
zsh -ic 'pnpm install'Then run the app from the repository root using two terminals.
Terminal 1 starts the React development server:
zsh -ic 'pnpm dev'Wait until Vite prints http://localhost:4173/. Terminal 2 starts the native
menu-bar host:
make engine
swift run --package-path macosA shield icon appears in the macOS menu bar. Click it to open the app. No Dock icon or regular window appears because Snapshotter runs as a menu-bar accessory. Stop the native process with Control-C. In a packaged build, right-click the menu-bar icon and choose Quit Snapshotter.
Debug builds enable WKWebView inspection. Right-click inside the popover and choose Inspect Element, or attach through Safari's Develop menu.
If the shield is not visible, check the menu-bar overflow area and confirm the Vite terminal is still running. Build each layer independently with:
zsh -ic 'pnpm build'
make engine
swift build --package-path macos
cd engine && go test -race ./...Before publishing a change, run the complete release gate:
make verifyThis checks Biome, UI tests, the production frontend, Go race tests, strict Swift compilation, app signing, the bundle plist, and DMG integrity.
Remote backends have an opt-in live lifecycle test. Supply a unique test
repository location and credentials through SNAPSHOTTER_TEST_REPOSITORY_*
environment variables, then run:
cd engine
go test ./resticadapter -run '^TestLiveRemoteRepository$' -count=1The test creates an encrypted repository, backs up and restores a file, checks the repository, then deletes the snapshot and prunes its data. Never target a production repository.
The host loads the Vite server in debug builds. Release packaging will bundle the generated web assets into the application.
Create a signed application bundle at build/Snapshotter.app:
make app
open build/Snapshotter.appPackaging automatically uses the first valid identity in the login Keychain. A specific identity can be selected for a distributable build:
CODESIGN_IDENTITY="Developer ID Application: Example (TEAMID)" make appUse CODESIGN_IDENTITY=- make app only for a throwaway build. Ad-hoc signatures
change when the application changes, so macOS cannot persist Keychain access for
them across builds.
Create a compressed installer image at build/Snapshotter.dmg:
make dmgFor release distribution, configure an Apple notary profile and sign with your Developer ID. The disk image is submitted, stapled, and verified automatically:
CODESIGN_IDENTITY="Developer ID Application: Example (TEAMID)" \
NOTARY_PROFILE="snapshotter-notary" make dmgThe .github/workflows/release.yml workflow builds a signed and notarized
.app (zipped) and .dmg on a macos-14 runner and publishes a GitHub Release.
Two channels are supported, selected automatically from the tag:
v1.2.3— stable release.v1.2.3-rc.1,v1.2.3-beta.1, ... (any tag with a hyphen pre-release part) — pre-release.
You can also trigger the workflow manually (Actions → release → Run workflow)
to build a pre-release from any ref, optionally pinning a tag via the tag input.
Full signing is required and only works when these repository secrets are configured:
MACOS_CERTIFICATE— base64 of aDeveloper ID Applicationcertificate.p12.MACOS_CERTIFICATE_PWD— the.p12password.MACOS_CERTIFICATE_ID— the identity name, e.g.Developer ID Application: Snapshotter (TEAMID).APPLE_ID— the Apple ID email used for notarization.APPLE_TEAM_ID— the 10-character team ID (also shown on the Developer ID certificate), e.g.CWYUH669FD.APPLE_APP_PASSWORD— an App Store account app-specific password created atappleid.apple.com→ Sign-In & Security → App-Specific Passwords. No App Store Connect API key or access request is needed.
Releases also embed a 16:9 changelog-aware banner rendered in CI
(Snapshotter-banner.png). Without AI it uses a static on-brand template. To
have the banner designed automatically from the changelog, additionally set:
OPENCODE_API_KEY— key for an OpenAI-compatible model endpoint.OPENCODE_BASE_URL— the endpoint base URL (e.g. an OpenAI-compatible API).OPENCODE_MODEL_ID— the model ID to send.
When all three are set, the workflow installs opencode2 and runs it with the
design-promotional-banner skill against the gathered changelog; if that fails,
the pipeline falls back to the static template so a release never ships without
a banner.
The app never shells out to the restic executable. The Go engine is linked into the native application and exposes a small asynchronous API. Credentials are never persisted in UI state. Keychain storage is handled by the native layer; optional 1Password storage uses the official Go SDK and the 1Password desktop app.
To use 1Password storage, install and sign in to 1Password for Mac, then enable Settings > Developer > Integrate with 1Password SDKs. Enable Touch ID under Settings > Security to authorize Snapshotter with biometrics. During repository setup, select 1Password, choose a detected account, authorize access, and choose a vault. On macOS, Snapshotter detects active accounts from 1Password's read-only local metadata and presents them by display name; manual entry remains available if detection is unavailable. Snapshotter stores only the account UUID, vault ID, and item ID in its preferences. The repository password and remote-backend credentials remain in the synced 1Password item. On another Mac, choose the same destination, load the same vault, and select the synced Snapshotter item instead of entering the secrets again. Snapshotter stores the repository type and destination in that item and adds recovery instructions to its notes, so the item can be used to reconnect after reinstalling macOS. Disconnecting a repository leaves its synced item in 1Password so other devices do not lose access; remove that item manually when it is no longer needed anywhere.
The official SDK loads 1Password's signed IPC client library from the desktop app. Because that library is signed by 1Password rather than Snapshotter, packaged builds use the hardened-runtime library-validation exception required for this integration.