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.pyError: 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/emptyError: /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:
| Platform | Default 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 darwinREFUSED 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 allThe local root must be a plain local path -- a hard rule of thepreservation design. A cloud-synced folder is welcome as the CONSOLIDATEDroot 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.
| File | Written by | Read by | Affects |
|---|---|---|---|
machines.json | installer only | sync, doctor, processing | Which UUID maps to which machine folder name. |
projects.json | installer seeds; sync appends; you edit | converter (grouping), never the sync’s routing | How raw encoded paths group into logical projects at process time. |
profiles.json | installer seeds; you edit | converter | What 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.
| Key | Type | Required | Default | Affects |
|---|---|---|---|---|
schema_version | number | no | 3 | Top-level document marker for the row shape documented here. |
key | string | yes | — | The 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_path | string | yes | — | The tool-native encoded folder name (e.g. -Users-you-code-myproject). The join point to the raw archive; never rewritten. |
git_repo | string | no | "" | Descriptive metadata: the project’s repository path. Round-tripped into manifests and consulted by the consolidation authoring helper; not used for routing or grouping. |
process | bool | no | false (auto-appended rows) | Nothing, by default — see below. Reserved for the future LLM/wiki phase; --respect-process-flag restores the opt-out semantic. |
consolidate_into | string | no | absent | Redirects 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:
| Key | Type | Required | Default | Affects |
|---|---|---|---|---|
description | string | no | "" | Human label. No functional effect. |
strip | string | no | "rules" | "none" means lossless passthrough; anything else applies the exclusion rules below. |
exclude_event_types | array | no | [] | JSONL event types dropped by name (e.g. progress, file-history-snapshot). |
exclude_content_blocks | array | no | [] | Content block types dropped by name (e.g. thinking, tool_use, image). |
drop_user_flags | array | no | [] | User-line flags that exclude a line (e.g. isMeta, isSidechain). |
keep_frontmatter | bool | no | true | Whether emitted files carry provenance frontmatter. |
drop_command_echoes | bool | no | false | Drops slash-command echo lines. |
strip_system_reminders | bool | no | false | Strips embedded <system-reminder> harness text. |
include_subagents | bool | no | true | Whether subagent transcripts are emitted alongside the session. |
version | number | no | 1 | Bump 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.
| Variable | Defaults for | Purpose |
|---|---|---|
PIBMO_ROOT | --root | The local dataset root. Read by sync, doctor, and the converter. |
PIBMO_SECONDARY | --secondary | Optional whole-root disaster-recovery mirror destination (accumulate-only). |
PIBMO_CONSOLIDATE | --consolidate | Consolidated root to push this machine’s subtree into (accumulate-only transport). |
PIBMO_LOCK_DIR | — | Lock 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):
| Platform | Unit |
|---|---|
| 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} -vroot: {root}machines: mac-studioprojects: 1 logical key(s) from 2 raw folder(s)profiles: faithful, dialoguekeys=1 profiles=2 wrote=4 unchanged=0 superset_dupes=0Two 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.