Skip to content

Configuration reference

Recall is configured by exactly one machine-local value — the dataset root, which travels on the scheduler unit’s command line — plus three shared JSON files at the top of that root. There are no dotfiles: nothing is read from ~/.pibmo and nothing is written into ~/.claude. This page documents every key exhaustively, with its type, default, whether it is required, and what it affects. The sources of truth are sync-transcripts.py (root resolution, auto-append), rootcheck.py (placement rules), and profiles.py / process_root.py (profile and grouping semantics); the values below are taken from them, and every quoted error, refusal, and warning was reproduced by running the shipped code.

The two roots

Recall distinguishes two roots, and they play different roles:

  • The local root (--root) is where preservation happens. It must be a plain local path — never a cloud-synced or network folder. Every machine has its own.
  • The consolidated root (--consolidate) is a transport destination: an accumulate-only copy of this machine’s subtree, pushed after each sync so several machines join into one queryable dataset. A cloud-synced folder is welcome here — by design, replication is its job, not preservation’s.

Both travel on the unit line. A single-machine setup needs only --root; consolidation never comes up.

The dataset root

The root is resolved in this order: the --root flag, then the PIBMO_ROOT environment variable. There is no third fallback — a run without either fails fast rather than guessing. Verified:

$ python3 scripts/sync/sync-transcripts.py
Error: no dataset root. Pass --root /path/to/dataset (or set PIBMO_ROOT). The installer prints the exact cron line.

A root that exists but was never registered also fails fast, with the fix named:

$ python3 scripts/sync/sync-transcripts.py --root /path/to/empty
Error: /path/to/empty/machines.json not found.
Run install.sh first to register this machine and seed the shared config.

Platform defaults

The installer proposes the platform default; rootcheck.py --print-default prints it:

PlatformDefault local root
macOS~/Library/Application Support/pibmo/recall
Linux / WSL${XDG_DATA_HOME:-~/.local/share}/pibmo/recall

The cloud refusal

rootcheck.py classifies a proposed local root and the installer refuses cloud-synced or remote paths for it. Reproduced as printed:

$ python3 scripts/sync/rootcheck.py --check-local-root "~/Dropbox/Datasets/pibmo" --platform darwin
REFUSED as the local dataset root: ~/Dropbox/Datasets/pibmo
- path component 'Dropbox' looks cloud-synced (dropbox)
Risks of a cloud-synced local root (the 2026-06/07 fleet outage class):
- schedulers can be permission-fenced off the path (macOS TCC denies
launchd and cron access to ~/Library/CloudStorage) -> total, silent outage
- the cloud client can evict files to dataless placeholder stubs the
sync cannot read
- client rewrites and conflict copies violate append-only preservation
- when the client is not running, nothing is preserved at all
The local root must be a plain local path -- a hard rule of the
preservation design. A cloud-synced folder is welcome as the CONSOLIDATED
root instead -- that is replication's job, not preservation's.
Detection is heuristic: re-run with --force to override eyes-open.

Detection is heuristic by design — custom cloud-folder locations are not all detectable — so the refusal pairs with a --force escape hatch: you override eyes-open instead of lying to the prompt. Symlinks are resolved before classification, so a local-looking alias into a cloud folder is still caught.

The three shared files

Shared configuration lives at the top of the root, not in the repo and not in home-directory dotfiles. The repo ships projects.example.json and profiles.example.json as templates; the installer seeds the live copies.

FileWritten byRead byAffects
machines.jsoninstaller onlysync, doctor, processingWhich UUID maps to which machine folder name.
projects.jsoninstaller seeds; sync appends; you editconverter (grouping), never the sync’s routingHow raw encoded paths group into logical projects at process time.
profiles.jsoninstaller seeds; you editconverterWhat each processed projection excludes.

On a multi-machine setup, the consolidated root keeps its own authoritative copies: the transport step pushes only this machine’s subtree and never copies the shared config across — other machines depend on the destination’s versions.

machines.json

Installer-owned. Maps each machine’s hardware UUID to the friendly name that becomes its folder under the root:

{ "machines": { "564D1234-ABCD-...": "mac-studio" } }

The sync reads this file and never writes it; each machine writes only its own {root}/{machine}/machine.json identity record. A machine whose UUID is absent refuses to sync — registration is the installer’s job, and an unregistered writer inventing its own folder name would fragment the archive.

projects.json

The process-time correspondence map. One row per encoded source path. Only key and encoded_path are functionally required — a row missing either is ignored by the grouping loader.

KeyTypeRequiredDefaultAffects
schema_versionnumberno3Top-level document marker for the row shape documented here.
keystringyesThe logical project name: the folder under {root}/processed/{key}/. Must be a single path token — a path-like key (/, \, ..) is refused with a warning and the row processes under its raw name.
encoded_pathstringyesThe tool-native encoded folder name (e.g. -Users-you-code-myproject). The join point to the raw archive; never rewritten.
git_repostringno""Descriptive metadata: the project’s repository path. Round-tripped into manifests and consulted by the consolidation authoring helper; not used for routing or grouping.
processboolnofalse (auto-appended rows)Nothing, by default — see below. Reserved for the future LLM/wiki phase; --respect-process-flag restores the opt-out semantic.
consolidate_intostringnoabsentRedirects this row’s sessions into another row’s key at process time. Chains resolve transitively; a cycle is broken deterministically with a warning.

Auto-appended rows

The sync appends newly seen encoded paths so the converter knows about them — this is for the converter’s benefit and is never used for routing. An appended row defaults to key == encoded_path, git_repo: "", process: false; you edit it at leisure. The rewrite is atomic: this file carries your hand-curated consolidation map, and a crash mid-write must never truncate it.

consolidate_into

To unify one logical project worked on under several paths (clones, worktrees, machines), give each extra row its own key plus consolidate_into set to the canonical row’s key. The converter resolves chains transitively (A into B, B into C lands A under C) and the merged output is deduped by superset when the same session was synced from several machines. Grouping happens entirely at process time: editing this key changes only where processed Markdown is emitted, never where raw data lives. Merging the raw archive folders themselves is a separate, explicit command (fold_archive.py, non-destructive, dry-run by default).

The process flag

The converter does not consult process by default: faithful and dialogue emission is cheap, local, and deterministic, and an institutional-memory system wants every project queryable — auto-appended rows all carry process: false, so honouring it would skip nearly everything. The flag is reserved for the future LLM/wiki phase where per-project cost is real; --respect-process-flag restores the old opt-out semantic.

Missing vs corrupt

A missing projects.json means no map: every encoded path processes under its raw name — a documented fallback, not an error. A corrupt file is a loud error and stops the run: silently degrading to identity mapping would fork the entire processed tree under raw names.

profiles.json

Optional overrides for processing profiles, merged per-key over the built-in defaults in profiles.py (DEFAULT_PROFILES). Denylist semantics: a profile declares what to exclude; everything else flows through — vendors add event types without notice, and a denylist includes an addition visibly until the config learns its name, where an allowlist would drop it silently.

Each entry under "profiles" is an object:

KeyTypeRequiredDefaultAffects
descriptionstringno""Human label. No functional effect.
stripstringno"rules""none" means lossless passthrough; anything else applies the exclusion rules below.
exclude_event_typesarrayno[]JSONL event types dropped by name (e.g. progress, file-history-snapshot).
exclude_content_blocksarrayno[]Content block types dropped by name (e.g. thinking, tool_use, image).
drop_user_flagsarrayno[]User-line flags that exclude a line (e.g. isMeta, isSidechain).
keep_frontmatterboolnotrueWhether emitted files carry provenance frontmatter.
drop_command_echoesboolnofalseDrops slash-command echo lines.
strip_system_remindersboolnofalseStrips embedded <system-reminder> harness text.
include_subagentsboolnotrueWhether subagent transcripts are emitted alongside the session.
versionnumberno1Bump to force re-emission after a ruleset change.

The built-in profiles

Two profiles ship as defaults and resolve even with no profiles.json at all:

  • faithful — lossless, nothing dropped (strip: "none").
  • dialogue — the human/AI conversation only: machinery event types, thinking and tool blocks, images, meta and sidechain lines, command echoes, and system reminders excluded by name. Lossy by design, for LLM/wiki ingestion.

Profile names are arbitrary config keys, so add your own freely. full is accepted as a true alias of faithful (its earlier name): an override under that key tunes faithful itself rather than materialising a duplicate lossless tree.

Guard rails

Three misconfigurations are caught loudly rather than reinterpreted. All three verified (paths abbreviated):

faithful is lossless by construction — an override may tune its metadata or subagent handling but can never demote it to a lossy profile:

profiles: 'faithful' override cannot make the faithful profile lossy (strip forced back to 'none')

The retired allowlist keys (keep_event_types, keep_content_blocks) are rejected per profile — silently reinterpreting old semantics could drop content:

profiles: ignoring 'old-style' from {root}/profiles.json -- retired allowlist key(s) keep_event_types (declare exclude_* instead)

And a malformed profiles.json is reported and skipped — the built-in defaults always resolve, so a broken override file can never halt processing or change what the faithful profile preserves. A strip: "none" profile that also declares exclusions gets a warning that those fields are inert.

Environment variables

Each variable is the default for the flag of the same name; the flag wins when both are set.

VariableDefaults forPurpose
PIBMO_ROOT--rootThe local dataset root. Read by sync, doctor, and the converter.
PIBMO_SECONDARY--secondaryOptional whole-root disaster-recovery mirror destination (accumulate-only).
PIBMO_CONSOLIDATE--consolidateConsolidated root to push this machine’s subtree into (accumulate-only transport).
PIBMO_LOCK_DIRLock directory for the concurrent-run guard (default /tmp).

Four further variables — PIBMO_MACHINE_UUID, PIBMO_CLAUDE_PROJECTS, PIBMO_GEMINI_DIR, PIBMO_CODEX_DIR — override the machine identity and the source directories. They exist for the test suite; leave them unset in production.

Where the flags live: the scheduler unit

There is no config file for the flags. The root and the consolidation target travel on the command line of a native scheduler unit that scheduler.py generates (--print to preview, --write to install, --check to verify):

PlatformUnit
macOS~/Library/LaunchAgents/com.pibmo.recall.sync.plist
Linux / WSL~/.config/systemd/user/pibmo-recall-sync.service + .timer

The unit points at the repo checkout and carries everything the sync needs — this ProgramArguments block is from a generated plist (paths abbreviated):

<key>ProgramArguments</key>
<array>
<string>/usr/bin/python3</string>
<string>/Users/you/code/pibmo-recall/scripts/sync/sync-transcripts.py</string>
<string>--root</string>
<string>/Users/you/Library/Application Support/pibmo/recall</string>
<string>--consolidate</string>
<string>/Users/you/Dropbox/Datasets/pibmo</string>
</array>

Because the unit runs the checkout directly, updating is git pull — there is no deployed copy and no self-updater. To change the root or add a consolidation target, regenerate the unit (or re-run the installer) rather than hand-editing; doctor.py cross-checks the unit and its embedded paths and prints the exact remediation when they drift.

Complete annotated example

A projects.json unifying a project and its worktree. Every key below was accepted by the shipped code and produced the behavior noted in the comments. JSON does not permit comments; strip them before use.

{
"schema_version": 3,
"projects": [
{
"key": "myproject", // processed output folder name
"encoded_path": "-Users-you-code-myproject", // join point to the raw archive
"git_repo": "/Users/you/code/myproject", // descriptive; not used for routing
"process": true
},
{
"key": "myproject-worktree", // its own primary key
"encoded_path": "-Users-you-code-myproject--worktrees-feature-x",
"git_repo": "",
"process": false, // not consulted by default
"consolidate_into": "myproject" // sessions land under myproject
}
]
}

Processing a root that contains both encoded folders prints (root path abbreviated):

$ /usr/bin/python3 scripts/process/process_root.py --root {root} -v
root: {root}
machines: mac-studio
projects: 1 logical key(s) from 2 raw folder(s)
profiles: faithful, dialogue
keys=1 profiles=2 wrote=4 unchanged=0 superset_dupes=0

Two raw folders, one logical key: the worktree’s sessions were emitted under processed/myproject/, in both profiles, and the raw archive was not touched. Note the worktree row’s process: false did not skip it — see the process flag above.

What configuration cannot do

None of these keys can reroute, rename, shrink, or delete anything in the raw archive. Grouping is a process-time projection; editing projects.json changes only where Markdown is emitted. The credential exclusions (oauth_creds.json, auth.json, and kin) are constants in the sync’s code, not configuration — there is no key that admits credentials into the archive, and equally the profile denylists govern only processed output, never what is preserved. The faithful profile cannot be configured into lossiness. If a knob to delete or thin the archive is what you are looking for, Recall is deliberately the wrong tool.

See also

  • Getting started — install and confirm the first archived tick.
  • Architecture — the two roots, the faithful mirror, and the append-only rules these keys serve.
  • CLI reference — every command and flag that consumes this configuration.
  • Troubleshooting — the doctor’s checks and their exact remediations.