Keep using Photos.app like you always do. Attic quietly backs up your originals and edits to an S3 bucket you control. One-way, append-only.
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

Pre-flight permission check before backup starts

Spawn ladder with an empty UUID list before processing any assets to
verify Photos and Automation permissions are available. If the check
fails, abort immediately with actionable instructions instead of
waiting until the first batch.

+37
+25
cli/src/commands/backup.ts
··· 75 75 const log = options.quiet ? () => {} : console.log.bind(console); 76 76 const logger = options.logger; 77 77 78 + // Pre-flight: verify ladder has required permissions before doing any work 79 + if (exporter.checkPermissions) { 80 + try { 81 + await exporter.checkPermissions(); 82 + } catch (error: unknown) { 83 + if (isPermissionError(error)) { 84 + const msg = error instanceof Error ? error.message : String(error); 85 + console.error(`\n ${msg}`); 86 + console.error( 87 + `\n Fix the permission issue and run \`attic backup\` again.\n`, 88 + ); 89 + return { 90 + uploaded: 0, 91 + failed: assets.length, 92 + skipped: 0, 93 + totalBytes: 0, 94 + errors: [], 95 + }; 96 + } 97 + // Non-permission errors from the probe are non-fatal — ladder may 98 + // not be installed yet, or Photos may not be set up. Let the normal 99 + // batch flow surface these errors with full context. 100 + } 101 + } 102 + 78 103 // Filter to pending assets 79 104 let pending = assets.filter((a) => !isBackedUp(manifest, a.uuid)); 80 105
+12
cli/src/export/exporter.ts
··· 30 30 ): Promise<ExportBatchResult>; 31 31 /** Hint to scale timeout for the next exportBatch call. Implementations may ignore. */ 32 32 setEstimatedBatchBytes?(estimatedBytes: number): void; 33 + /** Pre-flight check: verify ladder has required permissions (Photos, Automation). */ 34 + checkPermissions?(): Promise<void>; 33 35 } 34 36 35 37 /** Thrown when the ladder subprocess exceeds its timeout. */ ··· 232 234 baseTimeoutMs, 233 235 timeoutForBytes(estimatedBytes), 234 236 ); 237 + }, 238 + 239 + async checkPermissions(): Promise<void> { 240 + if (!stagingDirCreated) { 241 + await Deno.mkdir(stagingDir, { recursive: true }); 242 + stagingDirCreated = true; 243 + } 244 + // Spawn ladder with an empty UUID list — triggers pre-flight 245 + // permission checks without exporting anything. 246 + await spawnLadder(ladderPath, [], stagingDir, 30_000); 235 247 }, 236 248 237 249 async exportBatch(