cappy.

Managed storage and cleanup

Cappy keeps everything it generates in one managed workspace per project and deletes only files it can prove it created.

Where things go

The default root is .cappy/ in the project. Set workspace.root in cappy.config.json to use another directory. Cappy refuses a root that is a filesystem root, the home directory or one of its ancestors, or the project directory or one of its ancestors.

.cappy/
  cappy-workspace.json          ownership registry
  sessions/<session-id>/
    session.json                session metadata (freeform or scenario)
    replay.bin                  adapter replay payload, byte-for-byte
    timeline.json               events from a freeform recording
    master.<ext>                OBS master from `record --capture`
  captures/<capture-id>/
    master.<ext>                verified OBS master
    <role>.mp4|.jpg|.png        derivatives
    manifest.json               succeeded, failed, or cancelled
  comparisons/<comparison-id>/
    triptych.mp4                A | B | amplified difference
    frames.json                 per-frame SSIM and PSNR
    worst-<n>-a|b|diff.png      A, B, and difference at the worst moments
    manifest.json               succeeded, regressed, or failed
  logs/<correlation-id>.jsonl   one structured log per command
  cache/running/                run locks for in-flight commands

Add .cappy/ to .gitignore. cappy doctor warns when it is not ignored; Cappy never edits .gitignore itself.

Ownership

cappy-workspace.json records every managed file by root-relative path with its SHA-256 and size, plus imported and external files by absolute path. Since registry version 2, it also records retired take numbers (see below). A version 1 registry is upgraded the next time Cappy changes it, and older Cappy builds refuse a version 2 registry rather than reissue takes. Each change is applied under a short-lived lock file (cappy-workspace.json.lock) to the registry as it is on disk at that moment, so commands running at the same time never drop each other's entries.

Writes are atomic and never overwrite: a file is written to a temporary sibling and linked into place only if the target does not exist. OBS writes masters into its own recording directory; Cappy moves each verified master it requested into the workspace.

If cappy-workspace.json is corrupt, Cappy refuses to open the workspace rather than guess ownership.

Cleanup rules

cappy clean (below) and the library primitive behind it, ManagedWorkspace.remove in the private @uppercut-labs/cappy-internal-workspace workspace, delete only explicitly selected files, and report exactly what happened:

Cleanup never runs as a side effect of a capture.

cappy clean

cappy clean --failed --dry-run           # preview: failed, cancelled, and interrupted leftovers
cappy clean --failed                     # remove them
cappy clean cap_… ses_…                  # remove specific captures or sessions
cappy clean --older-than 30d             # everything older than 30 days
cappy clean --logs --older-than 7d       # old command logs only
cappy clean --all                        # every capture, session, comparison, and log

clean deletes as soon as it runs. --dry-run (-dr) runs the same selection and checks and changes nothing, not even creating a missing folder.

What each item covers:

Cleaning a capture never removes a session you recorded with cappy record.

Selectors:

IDs and selectors combine, but --older-than works only with selectors. The registry and cache/ are never touched.

What is protected:

Take numbers are never reissued. Cleaning a successful capture retires its take in the registry before any file is deleted. The next capture of that scenario (with the same parameters) or replayed session continues past it, and --take <retired> fails with TAKE_EXISTS.

Interrupted commands

record, run, and captured replay hold a run lock while they may leave a session active, and compare holds one while it runs. The next such command marks sessions left active by a command that is no longer running as failed, with a warning. A capture interrupted before its manifest was written has no manifest, so it is never counted as successful. Locks from other machines are never treated as dead.

Secrets

The OBS password is read from the environment variable named by obs.passwordEnv and used only to authenticate. It is never written to configuration, sessions, manifests, or logs; Cappy refuses to write a manifest that contains it.

Setup prompt

Read the guide