lambda-watcher

From a zip in Downloads to a diffable version

You keep downloading Lambda deployment packages as backups. Eight steps later, each one is an archived version with a manifest, a database row, and a git tag — and you can finally answer “what changed between the 2nd one and the 10th?”

pipx install lambda-watcher # then: lw setup
Trigger a .zip lands in ~/Downloads Result version N, archived and indexed Network calls none, ever

THE PROBLEMTwo zips of the same Lambda, and no way to compare them

Ask Unix what changed between two versions of one function and you get an answer you can’t read. Almost all of it is a dependency bump.

a plain recursive diff between v1 and v2
$ diff -rq v0001/code v0002/code | wc -l
61

…of which 56 are site-packages/. The five that mattered are buried,
and two of the real changes are not file changes at all.

Worse, the two most dangerous changes in that deploy don’t show up in a file diff in any form: the code started reading a new environment variable, and started calling a new AWS service. Those are the ones that break a deploy at 2am.

THE PIPELINE · 8 STEPSWhat happens when a zip lands

The order is the design. Deduplication happens before unpacking, unpacking before analysis, and the content check before anything is written. Two steps can end the run early — those are marked.

01

Notice the file, and wait for it

watcher.py

A folder watcher sees the new file and puts it in a queue. Before anything reads it, the tool waits until the file stops growing — same size, same timestamp, then one final open to confirm nothing still holds it.

Browsers finish a download by renaming foo.zip.crdownload to foo.zip — but not all of them do. Without the wait, you archive half a file.

02

Fingerprint the file

can exit here

Take a SHA-256 of the zip itself and check it against every download ever handled. Already seen this exact file? Record it and stop.

This is the cheap check, done before unpacking anything. It catches the literal duplicate — the same file dragged in twice, or a restart re-scanning the folder.

03

Work out which Lambda it is

identify.py

A ladder of eight strategies, tried in order, stopping at the first hit: an alias you taught it, a rule in your config, a get-function JSON sitting next to the zip, a function already in the archive, and finally the filename with browser junk stripped off.

Nine differently-named downloads land on one function. It records which strategy won and how confident it was — so a wrong guess is visible, and rename --alias fixes it permanently.

04

Unpack it safely

trust boundaryextract.py

Extract into a scratch directory, refusing anything dangerous: paths that escape the folder, absolute paths, symlinks, password-protected archives, and zip bombs — checked both from the archive’s index and again while writing.

A rejected archive goes to quarantine/ with a text file saying why. It is never silently dropped, and you can go read it.

05

Read the code

analysis/

Seven analysers run over the unpacked tree: runtime, handler, dependencies, environment variables the code reads, AWS services it calls, hardcoded credentials, and a file-by-file inventory. Every file is marked as yours or vendored.

Dependencies get read twice — what requirements.txt asks for, and what is actually shipped inside the zip. Only the second one really ran.

06

Fingerprint the contents

can exit here

Hash the sorted list of every file path paired with that file’s hash. Compare it to the newest archived version. Identical? Record it as unchanged and stop — no new version.

This is the step that makes the whole tool usable. Re-downloading unchanged code gives you a different file but the same contents, and this is what tells those apart.

07

Promote it to version N

store.py

The scratch directory is moved — not copied — into its permanent home, named for its sequence number and a short content hash: 0002-7f887035. A complete manifest.json is written beside it, and the original zip is kept as package.zip.

Staging lives on the same disk as the archive, so this is a rename rather than a second full copy of a large tree.

08

Index it and commit it

db.py · gitmirror.py

Every file, dependency, environment variable, service and finding is written into SQLite in a single transaction. Then the code is copied into that function’s own git repository and committed, tagged v0002.

The database is only an index — it can be thrown away and rebuilt from the manifests on disk. The directories are what’s true.

THE KEY IDEATwo fingerprints, two different questions

Steps 02 and 06 both hash something, and confusing them is the difference between a useful history and a folder full of noise.

zip_sha256  ·  step 02

A hash of the downloaded bytes. Fast, computed before unpacking. Two downloads of the same Lambda produce different values, because a zip carries timestamps and ordering that shift every time.

tree_hash  ·  step 06

A hash over sorted (path, file hash) pairs from the unpacked tree. It ignores zip metadata entirely, so two downloads of the same Lambda produce the same value. This is what decides whether a version is new.

Which gives three distinct outcomes, and the tool names all three rather than collapsing them:

new-version

The code actually changed. A version directory is created, indexed and committed.

new file · new contents
unchanged

A genuinely different download of identical code. Recorded in the log, but no version is created.

new file · same contents
duplicate-download

Byte-for-byte the same file as before. Caught at step 02, before anything is unpacked.

same file
lambda-watcher ingest ~/Downloads/order-processor*.zip
new order-processor v0001 (archived a new version)
new order-processor v0002 (archived a new version)
unchanged order-processor v0002 (identical to version 2)
duplicate-download ? (this exact file was already archived)

THE RESULTWhat ends up on disk

Nothing is created until it’s needed — the first download you ever archive builds this whole structure from an empty folder. Your Downloads folder is left untouched.

~/.lambda-watcher/ ├── index.db # rebuildable index — never the source of truth ├── logs/watcher.log ├── reports/ # generated HTML; index.html links it all ├── repos/ │ └── order-processor/ # git mirror: one commit per version, tagged v0001… └── functions/ └── order-processor/ └── versions/ ├── 0001-7fc98e0e/ # sequence number + content hash │ ├── code/ # the unpacked tree │ ├── manifest.json # the complete analysis │ └── package.zip # the original download └── 0002-7f887035/

Because every version directory carries a full manifest.json, the whole archive is portable and self-describing. Copy it to another machine, run reindex, and the database rebuilds itself from the manifests.

THE PAYOFFThe diff you actually wanted

The same two versions the diff -rq at the top choked on. The 55 changed dependency files collapse into three version numbers, and the two changes that never appear in a file diff at all get sections of their own.

lambda-watcher diff order-processor
╭──────────────────────────────────────────────────────────────────────╮
│ order-processor   v0001 → v0002                                      │
│ 2 added  2 modified  9 renamed  55 vendored (hidden)  +24 / -5 lines │
│ to see the 55 vendored files: lw diff order-processor --vendor       │
╰──────────────────────────────────────────────────────────────────────╯
  size     8.3 KB → 9.2 KB (+886 B)

Dependencies
   manager  package   from    to       origin
+  pip      pydantic  —       2.9.0    installed
~  pip      boto3     1.34.0  1.35.20  installed
~  pip      botocore  1.34.0  1.35.20  installed

  Env vars added: QUEUE_URL
  AWS services added: sqs
  ↑ these need to exist in the function's environment configuration

New findings
high  aws-access-key-id  config.py:6  AKIA…LE (20 chars)
high  stripe-key         config.py:7  sk_l…dc (32 chars)
 low  debug-flag         config.py:8  DEBUG = True

Files
   path                                                                 +  −  size
~  lambda_function.py                                                   9  2  +322
~  requirements.txt                                                     2  1   +17
+  config.py                                                           10     +235
+  helpers/__init__.py
→  {db → helpers/db}.py                                                 1      +33
→  site-packages/boto3-1.{34.0 → 35.20}.dist-info/ · 4 files, 1         1  1    +1
   edited
→  site-packages/botocore-1.{34.0 → 35.20}.dist-info/ · 4 files, 1      1  1    +1
   edited

Four things are happening there that a file diff can’t do. The 55 vendored files are hidden and explained as three version numbers instead. A file that moved and was edited is reported as one rename, not an unrelated delete plus add, and a directory that moved whole — each bumped package’s dist-info — is reported as one line naming both names and the count, rather than one row per file saying the same thing. A new environment variable and a new AWS service are called out as configuration you have to apply before this deploys cleanly. And the credentials that appeared between the two versions get their own section — recorded redacted, so the secret itself never enters the index.

The rename is the one worth looking at closely. db.py moved into a package and gained a line in the same deploy — the case a plain diff can only describe as an unrelated delete plus add:

one hunk of the same diff
─────────────────────────────── {db → helpers/db}.py ───────────────────────────────
--- a/db.py
+++ b/helpers/db.py
@@ -9,5 +9,6 @@
             "id": order["id"],
             "items": order["items"],
             "total": Decimal(str(order["total"])),
+            "status": "PENDING",
         }
     )

The same comparison renders as a self-contained HTML page, which is the one to send to someone else — --html --open on any diff, or lw report for the whole history with a page per step, which opens in your browser by itself. It is one file with the stylesheet inlined — the file icons are drawn into the page and the syntax highlighting is computed when it is written, so nothing is fetched — and it survives being emailed or dropped in a bucket. Clicking a file opens its diff in a panel beside the list rather than pushing the list down the page, so the code gets the width it wants and the next file is still one click away:

The HTML diff report for order-processor v0001 to v0002: tables for configuration impact and new findings, then the list of changed files with lambda_function.py selected, its diff open in a panel down the right-hand side of the page.
lambda-watcher diff order-processor --html · open the real one · or its index
the published copy masks the demo's two fake credentials in the file diff — nothing else is altered

Set up an AI model once and every report also opens on a plain-English account of the change: what the function now does differently, what is worth checking, and a deploy checklist whose ticks the page remembers. Every file it mentions is a button that opens that file's diff, each changed file carries a one-line note, and the history page lists what each release did rather than only how many files it touched. lw ai add sets up Anthropic, OpenAI, Azure OpenAI or a model on your own machine, checking it answers before saving it; from then on the watcher explains each new version in the background, and a report opened while that is happening reloads itself when the answer lands.

Nothing leaves the machine until you add a model. When it does, vendored packages are never sent, credential files like .env are named but not quoted, and anything that looks like a key is replaced first — lw explain FN --dry-run prints the exact request. Rate limits, overloads, timeouts and dropped connections are retried with backoff, honouring the service's own Retry-After; a request that still fails leaves the report saying why, with the command that tries again. lw ai settings --no-send-code sends only the shape of a change, and lw ai off stops all of it without deleting a key.

IN PRACTICEThe commands you’d actually use

Run lw setup once and leave it; everything else is reading what it archived. Every command below is expanded with the output it actually prints — these are captures, not sketches.

show is the one to reach for first, and the line worth noticing in it is 2 declared, 3 installed: requirements.txt names two packages and three arrived in the zip. The third came along as a transitive dependency, and it is the installed list — not the declared one — that describes what actually ran.

lw setupEverything in one go: a config, the background watcher, and any history already on disk. After that, lw start, lw stop and lw restart manage the watcher on their own.
lambda-watcher setup --no-service
lambda-watcher 0.3.0 — setting up

  ✓ wrote ~/.lambda-watcher/config.yaml
  ✓ archive at ~/.lambda-watcher
  ✓ watching ~/Downloads

  found 3 zip(s) already in your download folder(s)
  run lw backfill ~/Downloads to archive them

Skipping the background watcher. Run lw watch when you want it, or lw start to install it later.

That is the whole setup. Download a Lambda zip as you normally would; run lw to see what it caught.

  lw demo                   see what it does now, on a sample Lambda
  lw --install-completion   tab-complete function names in your shell
lw statusIs it running, and what has it caught? This is also what plain lw prints.
lambda-watcher status
lambda-watcher 0.3.0

  ○ not watching   ~/Downloads is not being archived
    1 function · 2 versions · 17.5 KB in ~/.lambda-watcher
    ✦ AI explanations not set up · lw ai add

  order-processor    v0002    just now    python  

  reports: ~/.lambda-watcher/reports/index.html · lw report

  lw setup                    watch your downloads folder from now on
  lw diff "order-processor"   what changed in the last version
lw demoSee the whole thing work on a sample Lambda, in a scratch archive of its own — nothing appears in yours. It goes on to print the comparison lw diff would, and where the HTML page is.
lambda-watcher demo --no-open
lambda-watcher 0.3.0 — a demo, on a sample Lambda

Three downloads of order-processor land in a scratch Downloads folder: a release, the next release, and that release downloaded again.

               new  order-processor v0001  order-processor.zip — archived a new version
               new  order-processor v0002  order-processor (1).zip — 2 added, 2 modified, 9 renamed, 55 vendored
                    +24/-5 lines · new: 1 env var, 1 AWS service, 3 secrets
                    report: ~/.lambda-watcher/demo/reports/order-processor/v0001-v0002.html
         unchanged  order-processor v0002  order-processor (2).zip — identical to version 2
lw watchThe same watcher in the foreground, printing each download as it lands; Ctrl-C stops it. lw setup runs it in the background instead.
lambda-watcher watch
lambda-watcher 0.3.0 — archiving into ~/.lambda-watcher
watching ~/Downloads. Press Ctrl-C to stop.
               new  order-processor v0001  order-processor.zip — archived a new version
               new  order-processor v0002  order-processor (1).zip — 2 added, 2 modified, 9 renamed, 55 vendored
                    +24/-5 lines · new: 1 env var, 1 AWS service, 3 secrets
                    report: ~/.lambda-watcher/reports/order-processor/v0001-v0002.html
         unchanged  order-processor v0002  order-processor (2).zip — identical to version 2
done
lw initWrite a commented config file you can edit. Optional — the defaults work.
lambda-watcher init
wrote ~/.lambda-watcher/config.yaml
archive root: ~/.lambda-watcher
watching:     ~/Downloads

Next: lw start
lw doctorCheck the config, watch folders, the watcher and its heartbeat, git and disk space. Every problem names its fix, and it exits nonzero when there is one.
lambda-watcher doctor
check         status         detail                                            what to do  
config file   ok             ~/.lambda-watcher/config.yaml                                 
archive root  ok             ~/.lambda-watcher                                             
watch dir     ok             ~/Downloads                                                   
watcher       not installed  nothing is archiving in the background            lw setup    
heartbeat     none           the watcher has not reported in                               
git mirror    ok             enabled                                                       
ai            not set up     optional — explains each change in plain English  lw ai add   
index         ok             0 function(s), 0 version(s)                                   
disk free     ok             29.8 GB
lw ingest FILE...Archive zips by hand, in the order you pass them.
lambda-watcher ingest ~/Downloads/order-processor*.zip
new order-processor v0001 (archived a new version)
new order-processor v0002 (archived a new version)
unchanged order-processor v0002 (identical to version 2)
duplicate-download ? (this exact file was already archived)
lw backfill DIRImport a folder of old backups, oldest first, so version numbers match history. --dry-run shows the names it would assign.
lambda-watcher backfill ~/Downloads --dry-run
file                     modified          would become     via              
order-processor.zip      2026-09-04 05:52  order-processor  filename/medium  
order-processor (1).zip  2026-09-04 05:52  order-processor  filename/medium  
order-processor (2).zip  2026-09-04 05:52  order-processor  filename/medium  
3 file(s). Re-run without --dry-run to import.
lw lsEvery function archived so far.
lambda-watcher ls
function         versions  latest  last seen         runtime  
order-processor         2   v0002  2026-09-04 05:53  python   
lw versions FNEvery archived version of one function, newest first.
lambda-watcher versions order-processor
order-processor
version  archived          files    size  handler           downloaded as    label
v0002    2026-09-06 15:35     68  9.2 KB  lambda_function…  order-processor
                                                            (1).zip
v0001    2026-09-06 15:35     61  8.3 KB  lambda_function…  order-processo…

Compare the last two: lw diff "order-processor"
lw show FN [V]Everything one version contains: runtime, handler, dependencies, env vars, services and findings.
lambda-watcher show order-processor latest
order-processor v0002
  archived   2026-09-06 15:35
  source     order-processor (1).zip
  runtime    python (high confidence)
  handler    lambda_function.lambda_handler
  contents   68 files, 9.2 KB (5 first-party, 57 lines)
  tree hash  7f887035711ce470
  location   ~/.lambda-watcher/functions/order-processor/versions/0002-7f887035

Dependencies  2 declared, 3 installed
  boto3 1.35.20
  botocore 1.35.20
  pydantic 2.9.0

Environment variables read
  QUEUE_URL, TABLE_NAME

AWS services used
  dynamodb, sqs

Findings
    high aws-access-key-id config.py:6 AKIA…LE (20 chars)
    high stripe-key config.py:7 sk_l…dc (32 chars)
     low debug-flag config.py:8 DEBUG = True
lw diff FNCompare two versions — the last two unless you say otherwise. Add --html --open for the report below.
lambda-watcher diff order-processor
╭──────────────────────────────────────────────────────────────────────╮
│ order-processor   v0001 → v0002                                      │
│ 2 added  2 modified  9 renamed  55 vendored (hidden)  +24 / -5 lines │
│ to see the 55 vendored files: lw diff order-processor --vendor       │
╰──────────────────────────────────────────────────────────────────────╯
  size     8.3 KB → 9.2 KB (+886 B)

Dependencies
   manager  package   from    to       origin
+  pip      pydantic  —       2.9.0    installed
~  pip      boto3     1.34.0  1.35.20  installed
~  pip      botocore  1.34.0  1.35.20  installed

  Env vars added: QUEUE_URL
  AWS services added: sqs
  ↑ these need to exist in the function's environment configuration

New findings
high  aws-access-key-id  config.py:6  AKIA…LE (20 chars)
high  stripe-key         config.py:7  sk_l…dc (32 chars)
 low  debug-flag         config.py:8  DEBUG = True

Files
   path                                                                 +  −  size
~  lambda_function.py                                                   9  2  +322
~  requirements.txt                                                     2  1   +17
+  config.py                                                           10     +235
+  helpers/__init__.py
→  {db → helpers/db}.py                                                 1      +33
→  site-packages/boto3-1.{34.0 → 35.20}.dist-info/ · 4 files, 1         1  1    +1
   edited
→  site-packages/botocore-1.{34.0 → 35.20}.dist-info/ · 4 files, 1      1  1    +1
   edited
lw report [FN]Build a browsable HTML history: an index plus a diff for every step, and open it in your browser. Without FN, one page linking every function instead.
lambda-watcher report order-processor
wrote ~/.lambda-watcher/reports/order-processor/index.html (2 versions)
lw explain FNHave an AI model explain a change in plain English — what the function now does differently, what is worth checking, and a checklist for deploying it — printed here and added to the report, where every file it names opens that file's diff. --dry-run prints exactly what would be sent, and sends nothing.
lambda-watcher explain order-processor --dry-run (the start of it)
# What `lw explain` would send to your default model (none is set up yet; `lw ai add`).
# 2,870 characters · 5 of 5 changed files with their lines · 2 values redacted. Nothing was sent.

----- the change -----
# Lambda function 'order-processor': version 1 → version 2

- Older: v0001, archived 2026-09-24 17:39, downloaded as order-processor.zip
- Newer: v0002, archived 2026-09-24 17:39, downloaded as order-processor (1).zip
- Runtime: python (unchanged)
- Handler: lambda_function.lambda_handler (unchanged)
- Package: 61 files, 8.3 KB → 68 files, 9.2 KB
- First-party files changed: 2 modified, 2 added, 1 renamed; +22 −3 lines
- Vendored third-party files changed: 63 (not shown; the dependency changes below account for them)

## Dependency changes
- pydantic (pip, installed): added at 2.9.0
- boto3 (pip, installed): 1.34.0 → 1.35.20
- botocore (pip, installed): 1.34.0 → 1.35.20
lw aiSet up the models that explain your changes — Anthropic, OpenAI, Azure OpenAI, or one on your own machine — and choose when they run. lw ai add walks you through it and checks the model answers before saving; lw ai use, lw ai remove, lw ai test, lw ai settings and lw ai off do the rest.
lambda-watcher ai
AI explanations  not set up

  A model can read each change and explain it in plain English: what the
  function now does differently, what could break, and what to do before
  deploying — in the terminal and in every report. It works with Anthropic,
  OpenAI, Azure OpenAI or a model on your own machine, and nothing is sent
  anywhere until you set one up.

  lw ai add   set one up — it takes a minute
lw export FN VGet a version back out as a deployable zip. This is your rollback.
lambda-watcher export order-processor 1 -o rollback.zip
wrote ~/rollback.zip (17.4 KB)
lw open FN [V]Open the mirror in your editor — one folder named after the function, with every version in its history.
lambda-watcher open order-processor --print
~/.lambda-watcher/repos/order-processor
lw git FN ...Run git inside that function's mirror, where git diff v0002 v0010 just works.
lambda-watcher git order-processor log --oneline
7bdc51c order-processor v0002
12f17f7 order-processor v0001
lw path FN [V]Print where something lives, for cd "$(lw path order-processor)".
lambda-watcher path order-processor --repo
~/.lambda-watcher/repos/order-processor
lw search TERMFind a filename or package across every function and version at once.
lambda-watcher search boto3
Files
function         version  path                                                size
order-processor  v0002    site-packages/boto3/__init__.py                     24 B
order-processor  v0002    site-packages/boto3-1.35.20.dist-info/INSTALLER      4 B
order-processor  v0002    site-packages/boto3-1.35.20.dist-info/METADATA      83 B
order-processor  v0002    site-packages/boto3-1.35.20.dist-info/WHEEL         91 B
order-processor  v0002    site-packages/boto3-1.35.20.dist-info/top_level.t…   6 B
order-processor  v0001    site-packages/boto3/__init__.py                     23 B
order-processor  v0001    site-packages/boto3-1.34.0.dist-info/INSTALLER       4 B
order-processor  v0001    site-packages/boto3-1.34.0.dist-info/METADATA       82 B
order-processor  v0001    site-packages/boto3-1.34.0.dist-info/WHEEL          91 B
order-processor  v0001    site-packages/boto3-1.34.0.dist-info/top_level.txt   6 B
Dependencies
function         version  package  version
order-processor  v0002    boto3    1.35.20
order-processor  v0001    boto3    1.34.0
lw logRecent activity — including the downloads that were skipped, and why.
lambda-watcher log
when              event               function         detail
2026-09-06 15:35  duplicate-download  -                order-processor (2).zip
                                                       {"zip_sha256":
                                                       "707f02c2dae94945d6d2530d1…
2026-09-06 15:35  unchanged           order-processor  order-processor (2).zip
                                                       {"seq": 2}
2026-09-06 15:35  new-version         order-processor  order-processor (1).zip
                                                       {"seq": 2, "tree_hash":
                                                       "7f887035711ce470b395beef9…
2026-09-06 15:35  new-version         order-processor  order-processor.zip
                                                       {"seq": 1, "tree_hash":
                                                       "7fc98e0e8bb36d603d0286a21…
lw logsThe watcher's own log file — what it noticed and what it made of it. -f follows it; --service shows the service manager's output instead.
lambda-watcher logs -n 4
~/.lambda-watcher/logs/watcher.log

2026-09-22 09:32:17 INFO    archived order-processor v0002 (68 files, 9.2 KB)
2026-09-22 09:32:17 INFO    order-processor (2).zip -> function 'order-processor' (known-function, high confidence)
2026-09-22 09:32:17 INFO    order-processor (2).zip is byte-identical to order-processor v0002 - nothing new to archive
2026-09-22 09:32:17 INFO    skipping order-processor (2).zip: identical download already archived
lw label FN V TEXTAnnotate a version so you can recognise it later.
lambda-watcher label order-processor 2 "prod deploy 2026-03-01"
labelled order-processor v0002: prod deploy 2026-03-01
lw rename OLD NEWFix a misidentified name. --alias makes the fix stick for future downloads.
lambda-watcher rename order-processor orders-api --alias order-proc
renamed order-processor → orders-api
future downloads containing 'order-proc' will map here automatically
lw merge SRC DSTFold two entries that are really the same Lambda into one, renumbering by when each version was archived.
lambda-watcher merge order-processor-old order-processor
merged order-processor-old into order-processor (3 versions, renumbered by archive time)
lw reindexRebuild index.db from the manifests on disk. The archive is the source of truth.
lambda-watcher reindex --yes
reindexed 1 function(s), 2 version(s)
lw rm FNDelete a function and everything archived for it, mirror included. --yes skips the question.
lambda-watcher rm order-processor --yes
deleted order-processor

WORTH KNOWINGWhere the boundaries are

Only the code is archived. A deployment package doesn’t contain the function’s configuration — memory, timeout, environment variable values, IAM role, layers or triggers. The tool infers what it can from the code and flags it. Save aws lambda get-function --function-name X > X.json next to the zip and it gets archived too — and doubles as a naming hint at step 03.

Secret scanning is a tripwire, not a security tool. It catches the obvious cases and skips placeholders. Treat a finding as a reason to look, not a verdict.

No AWS API calls, ever. The tool only reads files already sitting on your disk. It needs no credentials and works offline. That’s a deliberate boundary, not a missing feature.

Run the examples yourself. Every capture on this page comes out of one script, against the real pipeline — no AWS account, no network. It builds the order-processor zips, ingests them, and prints each block in the order you just read them:

python3 -m venv .venv && .venv/bin/pip install -e ".[dev]" .venv/bin/python docs/examples/build_demo.py

The zips are stamped with a pinned build time, so the version directories come out as 0001-7fc98e0e and 0002-7f887035 on your machine too — the same ones quoted above.

Deeper reading: how the examples are built, design notes on why the analysis and diff layers work the way they do, and autostart recipes for launchd, systemd and Task Scheduler.