---
type: note
alexandria_core: true
tags: [screenshots, reference]
author: alexandria
---

# Ingesting Photos screenshots (local — read-only source)

The `screenshots` module sweeps **new screenshots from the owner's local macOS
Photos library** into the vault, in two deliberately separated stages: a
**capture** that makes the pixels durable, then a **read-and-triage** pass that
files what matters and drops the rest. Screenshots are where valuable things
get caught mid-scroll — posts, articles, receipts, bookings, charts, addresses,
ideas — and where a lot of noise accumulates; the pipeline keeps the first and
sheds the second.

The capture (`alexandria_vault.engine.screenshots_capture`) is **plumbing**.
The owner never runs it or sees a command — they say "digest my screenshots"
and the agent does the rest.

## The pipeline

1. **Photos → `Agent/inbox/` (durable).** The capture finds screenshots that
   are NEW since the last run and copies them out of the library: image files
   to `Agent/inbox/Files/Screenshots <stamp>/`, plus one `type: inbox-item`
   note holding a stub manifest (one row per shot, route `_pending triage_`)
   and the embedded images — the standard queue shape from
   [[AGENTS#Processing the inbox (fan-out)|the inbox workflow]].
2. **Commit the capture, then verify.** The script never commits; the agent
   commits the capture immediately — *before* any triage — and **verifies
   `git show --stat HEAD` lists every captured image** plus the inbox item.
   That check is the durability guarantee: capture-first means nothing is
   stranded if the session dies mid-triage.
3. **Triage by reading the images.** The agent opens each exported screenshot
   and sorts **valuable vs noise**. This is a judgment pass over pixels, not
   filenames — a screenshot's worth is only visible by looking at it.
4. **Route via the ingestion workflow.** Valuable content is decomposed and
   routed per [[AGENTS#Ingesting and filing content]] — facts, todos, people,
   events, Knowledge — with the image itself kept in an area's `Files/` only
   when the pixels are worth keeping (a chart, a document). The manifest is
   presented for the owner's approval, applied in **one ingest commit**
   (`ingest: YYYY-MM-DD screenshots (N filed, M dropped)`), and the inbox item
   + batch folder are deleted — the queue holds only pending work; git history
   preserves everything.

Sensitive captures (banking screens, IDs, credentials) are summarized, never
transcribed as raw values.

## Read-only guarantees

- **The live library is never opened.** `Photos.sqlite` (+`-wal`/`-shm`) is
  snapshot-copied to a temp dir and the *copy* is queried, so photolibraryd's
  locks are never contended and the library is never touched.
- **Images are copied out, never moved.** Nothing in Photos is altered,
  tagged, hidden, or deleted — and no write path exists to improvise.
- The only writes are the capture itself (inbox item + batch folder) and the
  script's own watermark state at `Agent/scripts/screenshots/state.json`
  (created fresh on first run; committed with the vault so incremental runs
  survive across sessions).

## Where it runs — locally, not on the automation host

The Photos library lives on the machine signed into the owner's iCloud Photos —
their **Mac** — not an always-on runner. Like the iMessage reader (and unlike
the digest/sweep engine), this capability runs *in-session on the local
machine*. macOS only.

## Prerequisite — Full Disk Access (one-time)

macOS TCC blocks reading the Photos library until the **app running the
assistant** has Full Disk Access. Grant it to Claude (or the terminal the agent
runs in): **System Settings → Privacy & Security → Full Disk Access → enable it
→ re-open the app.** Until then the capture exits with that instruction. This
is a permission grant, not a stored credential.

## Semantics (worth knowing before triaging)

- **What counts as a screenshot:** `ZASSET.ZKINDSUBTYPE = 10`, trashed and
  hidden excluded — in practice, phone/Mac screenshots synced via iCloud
  Photos. Screen *recordings* (videos) are out of scope.
- **Incremental by added-date.** Runs window on `ZADDEDDATE` (when the shot
  *entered* the library), so a screenshot taken offline still gets caught when
  it syncs late. The watermark + a seen-UUID set live in `state.json`;
  `--since` overrides the window without ever moving the watermark backwards.
- **Originals are often not on disk.** With iCloud "Optimize Mac Storage" the
  capture prefers the true original and falls back to the **largest local
  derivative** (full device resolution — text readable); each capture is
  labeled `original` / `full-res derivative`. Shots with no usable local file
  (a derivative under ~40KB is a thumbnail, not a render) are reported and
  **retried automatically on later runs**.
- **Filenames carry the shot's own timezone.** `Screenshot YYYY-MM-DD HHmm
  (<original name>).ext`, named by *taken* time in the asset's own GMT offset,
  so days stay right across travel. The rare HEIC is converted to JPEG (via
  `sips`) so agents can read it.
- **Backfill = re-run the same `--since` window.** `--limit` counts *captured*
  shots (not-local rows never wedge a batch), the seen-set skips everything
  already captured, and the watermark never moves backwards — so repeated runs
  of e.g. `--since 90d --limit 25` walk oldest-first through history one
  triageable batch at a time.
- **The script never commits.** The capture commit, the `git show --stat`
  durability check, triage, and the eventual ingest commit are the agent's job
  (the `screenshots` skill).

## The capture (flags, for the agent)

```sh
P="${ALEXANDRIA_PYTHON:-python3}"   # the interpreter with the alexandria-vault package
"$P" -m alexandria_vault.engine.screenshots_capture --vault .              # new since last run → inbox
"$P" -m alexandria_vault.engine.screenshots_capture --vault . --dry-run    # list what would be captured; write nothing
"$P" -m alexandria_vault.engine.screenshots_capture --vault . --since 3d   # explicit window: 3d | 2w | today | yesterday | YYYY-MM-DD | all
"$P" -m alexandria_vault.engine.screenshots_capture --vault . --limit 10   # cap the batch (0 = no cap); excess reported as remaining
"$P" -m alexandria_vault.engine.screenshots_capture --vault . --json       # machine-readable report (prefer for synthesis)
```

Every run prints a one-line coverage summary to **stderr** — `window <desc> →
found N new · captured M · K not-local (retry queued) · R remaining beyond
--limit` — so `--json` stdout stays parseable. Read it before synthesizing: a
non-zero *remaining* means the batch was capped, not complete.

## Optional config (`Agent/config.json` → `screenshots`)

All optional; built-in defaults apply when absent.
- `screenshots.default_since` — first-run window when no watermark exists yet (default `"7d"`).
- `screenshots.default_limit` — max shots captured per batch (default `75`).
- `screenshots.library` — path to the `.photoslibrary` (default `~/Pictures/Photos Library.photoslibrary`).
