Skip to content

Configuration files

No config files are hand-written — every file below is created and updated by forest commands. This page documents what they contain so you can read them in review and commit the right ones.

Workspace registry — committed

.forest/config.yaml
version: 1
checkouts: {}      # populated as checkouts are registered

Checkout forest.yaml — shared, committed

One per checkout, at .forest/checkouts/<name>/forest.yaml:

.forest/checkouts/demo/forest.yaml
project: demo
remotes:
  origin:
    url: s3://my-bucket/prefix
    region: us-east-2          # optional; also endpoint, profile, key_file, known_hosts
stages:
  raw:
    remote_path: demo/raw      # optional; defaults to <checkout>/<stage>
    sync_by: subdirectory      # subdirectory | directory | file
    direction: both            # push | pull | both (default: both, omitted)

direction restricts which transfer verbs may touch a stage: bare forest push/forest pull (and --all) skip a wrong-direction stage with an info note, while explicitly selecting one (--stage, unit IDs, --id) is an error. There is no flag override — changing direction is a deliberate forest.yaml edit (ADR 0022).

Checkout local.yaml — per-machine, gitignored

.forest/checkouts/demo/local.yaml
active_remote: origin
stage_paths:
  raw: ../data/raw             # relative resolves from the workspace root

Checkout sync_state.json — per-machine, gitignored

One per checkout, written by push and pull after each verified transfer. Each entry records what moved, when, and the evidence forest uses for skip decisions, dirty-pull guards, and force-with-lease checks (trimmed real entry):

.forest/checkouts/survey/sync_state.json
{
  "remote": "origin",
  "stage": "raw",
  "unit_id": "plot-01",
  "direction": "push",
  "timestamp": "2026-07-02T22:32:44.641369+00:00",
  "file_count": 2,
  "total_bytes": 73,
  "checksum": "42ca3c31aea092c220ba8b85369bca1a666a91d35d6578cc4c5db925ff25b10e",
  "file_snapshot": { "notes.txt": 43, "temps.csv": 30 },
  "file_hashes": {
    "notes.txt": "01642dd14166e068f3c35fe75b82d187cf67969041ea814e94f7ead1b589f121",
    "temps.csv": "b150ba5104379dc6d21c4d082ba75a2fad36c4acc8ad1176eaba8d340a1aabbb"
  }
}
  • checksum — aggregate over the sorted relpath:size:sha256 lines; content-sensitive, so a same-size edit changes it.
  • file_snapshot{relative path: size} for every file in the unit at transfer time; the fast comparison status uses by default.
  • file_hashes — per-file SHA-256, checked by status --checksum, the dirty-pull guard, push's skip decision, and pull's proof that the local copy used for a live remote comparison is still clean.

Entries written before forest recorded content hashes lack these fields; they read as stale, re-push once after upgrading, and heal to the full shape.

Which files does git see?

File Scope Git
.forest/config.yaml shared committed
.forest/checkouts/<name>/forest.yaml shared committed
.forest/HEAD user-local gitignored
.forest/checkouts/<name>/local.yaml user-local gitignored
.forest/checkouts/<name>/sync_state.json user-local gitignored

The .gitignore entries are managed by forestforest init writes them. A fresh clone restores the local half with forest bind + forest remote use + forest pull (worked example).