Host integration
The scheduler archives transcripts; host integration is what tells you it is still doing so. Without it, a LaunchAgent or timer can die quietly — an OS update, a moved checkout, a revoked permission — and nothing notices until the tools’ rotation windows have already eaten sessions. The integration is a single session-start hook that prints a context banner into the session: who, where, and on what machine this session runs, plus one line on whether this machine’s archive is alive. The banner also stamps the transcript itself, so every archived session opens with its own provenance. This page documents what the wiring script writes, what the banner looks like, why its failure modes are loud, and how to install without it, verify it, and remove it.
For where this fits in setup, see Getting started and Onboarding. For the doctor the banner’s warnings point to, see the CLI reference.
What the wiring script writes
The banner is not part of install.sh — the installer writes nothing outside
the dataset root and the scheduler unit. Wiring the banner is a separate,
optional step:
bash ~/code/pibmo-recall/scripts/maintenance/deprecate-sync-hooks.shRun it from a real terminal, not from inside a Claude Code session — the
session’s command sandbox blocks writes to ~/.claude/settings.json.
Two files are involved, both under ~/.claude:
| Path | Role |
|---|---|
~/.claude/settings.json | Gains a SessionStart entry that runs scripts/hook/pibmo-context.py from the repo checkout. Created if absent; an existing file is backed up to settings.json.bak-<timestamp> first. Merged additively; hooks that are not Pibmo’s are left untouched. |
~/.claude/machines.json | The banner’s private UUID-to-name map — written by the hook itself, not the script. The first session self-registers this machine’s hardware UUID; edit the value to a friendly name to make the banner show it. It is deliberately separate from the sync registry {root}/machines.json, which the hook cannot assume it can reach (see Configuration). |
No hook script is copied anywhere. The entry points straight at the checkout,
so git pull updates the banner like everything else — there is no deployed
copy and no self-updater.
The registered SessionStart entry looks like this (the absolute path of your
checkout is embedded at wiring time):
{ "hooks": { "SessionStart": [ { "matcher": "", "hooks": [ { "type": "command", "command": "python3 \"/Users/you/code/pibmo-recall/scripts/hook/pibmo-context.py\"" } ] } ] }}The merge is idempotent. Re-running the script detects the entry by its exact
command string and does not add a duplicate. The same pass also migrates: any
pre-reset Pibmo hooks — the old sync hooks, the old banner, or a banner wired
to a deployed copy under ~/.claude/scripts/ — are stripped, and the stale
deployed copies are deleted. If settings.json exists but is not valid JSON,
the script stops on the parse error rather than rewrite a file it cannot read;
the backup it has just taken is left in place.
The banner
At session start the hook prints a context block. On a healthy macOS machine, inside a git checkout:
=== Session context ===Time: 2026-07-22T06:02:53+10:00Machine: moraba (5A2B9C1D…) [macOS] host=moraba.localUser: youFolder: pibmo-recall (/Users/you/code/pibmo-recall)Git: main @ fa29cca https://github.com/attilabalogh/pibmo-recall.gitLast: fix(sync): antigravity-ide is a data dir -- include it minus media/binariesArchive: OK — last sync 0.4h ago (via com.pibmo.recall.sync.plist)Session: 9f31c2aa-7c4e-4b8a-9d21-0b6f3f6f2a41 (startup)=======================Optional lines appear only when they resolve: Project: when a nearby manifest
(package.json, pyproject.toml, Cargo.toml, go.mod) names the project,
Git: and Last: only inside a git work tree, and Archive: only when this
machine has a schedule installed. A machine that never enrolled prints no
Archive: line at all — the banner never guesses.
The Archive: line is the liveness check — it watches the watchers, with no
configuration of its own. The dataset root is discovered from the installed
schedule (the root travels on the unit line, not in a dotfile — see
How it works), the machine name from
{root}/machines.json, and the heartbeat from {root}/{machine}/pibmo.db. The
via suffix names the discovery source: the LaunchAgent
com.pibmo.recall.sync.plist on macOS, pibmo-recall-sync.service on Linux,
or crontab when running on the fallback line.
Routine stays one quiet line; trouble gets two — the warning, then an indented
fix: line carrying the exact remediation, matching the doctor’s status + fix
grammar:
Archive: ⚠ STALE — last sync 31.2h ago (max 26) fix: python3 /Users/you/code/pibmo-recall/scripts/sync/doctor.py --root "/Users/you/Library/Application Support/pibmo/recall"| Warning | Meaning |
|---|---|
⚠ STALE — last sync 31.2h ago (max 26) | A schedule exists but no sync has completed in over 26 hours — the threshold is one hourly tick plus a slept-through laptop day. |
⚠ no sync has ever recorded a run | A schedule is installed but the heartbeat database has no runs; the scheduler was never activated, or every run is dying. |
⚠ this machine is not registered in {root}/machines.json | The schedule names a root, but this machine’s hardware UUID is missing from the registry there. The fix line says: run install.sh on this machine. |
⚠ root {root} unreachable from this session (...) | The root on the unit line cannot be read from here — an unmounted disk, a moved folder, a permission fence. |
⚠ liveness check errored: ... | Any other failure, surfaced with its exception type rather than hidden. |
It never fails a session start
The hook always exits 0. An archive check must never be the reason a session fails to start — but unlike a hook that merely swallows errors, this one surfaces them (the project invariant is never swallow). Every failure degrades to something visible, not to silence:
- A missing hook payload on stdin (a manual run) degrades the
Session:fields to a visible?. - A failed hardware-UUID probe prints its cause to stderr and shows
unknown. - An unreadable
~/.claude/machines.jsonfalls back to showing the raw UUID. - Git and project-name probes that find nothing simply omit their lines — optional context, legitimately absent.
- Any exception inside the liveness check is caught and printed into the banner
as
Archive: ⚠ liveness check errored: ..., with the exception type.
The practical consequence: a broken archive degrades to a loud warning line, and a broken banner degrades to a partial banner — never a blocked session.
Everything printed is local
Every line of the banner originates on this machine; nothing authored by a remote party is fetched to build it:
| Line | Source |
|---|---|
Time, User, Folder | The local clock, environment, and working directory. |
Machine | The hardware UUID (ioreg, /etc/machine-id, or the Windows registry) mapped through ~/.claude/machines.json. |
Project | The name field of the nearest package.json, pyproject.toml, Cargo.toml, or go.mod. |
Git, Last | git run against the working directory: branch, short SHA, dirty flag, origin URL, and the last commit subject. |
Archive | The scheduler unit or crontab line, {root}/machines.json, and the sync heartbeat in {root}/{machine}/pibmo.db. |
Session | The JSON payload Claude Code pipes to the hook on stdin. |
One honest caveat: the Last: line prints the most recent commit subject
verbatim, and Project: prints a manifest’s name field verbatim — the hook
does not sanitize either. In your own repositories that is your own text; in a
repository with other contributors, a commit subject is third-party text
entering the session context unchanged. Treat it as what it is — repository
content, not instructions.
Installing without the banner
The banner is opt-in, and the default install is without it: install.sh
writes nothing into ~/.claude, and archiving is fully functional the moment
the scheduler unit is active. Skipping the wiring step costs exactly one
thing — the per-session staleness warning. The liveness question is then
answered on demand instead:
python3 ~/code/pibmo-recall/scripts/sync/doctor.py --root <ROOT>Choose this if your host is not Claude Code, or if you wire session startup
yourself — the hook is plain stdlib Python reading a small JSON payload
(session_id, source, cwd) on stdin, so any mechanism that runs
scripts/hook/pibmo-context.py at the start of a session reproduces the
banner.
Verifying the hook fires
Run the hook directly. The < /dev/null stands in for the payload the host
would pipe in; without it, an interactive run waits on stdin:
python3 ~/code/pibmo-recall/scripts/hook/pibmo-context.py < /dev/nullecho "exit: $?"A manual run has no payload, so the Session: line degrades to ? (?) —
every other line, including Archive:, behaves exactly as at session start,
and the exit code is 0. To confirm the wiring inside the host, open /hooks
in Claude Code or restart it; the wiring script’s final line tells you the
same.
If the banner shows the raw UUID where the machine name should be, edit the
value in ~/.claude/machines.json to the friendly name. If the Archive:
line carries a warning, the indented fix: line under it is the remediation —
usually running the doctor, which performs the full battery
(registration, permission canary, unit paths, heartbeat, replication lag) of
which the banner’s line is the always-on subset. See
Troubleshooting.
Removing the hook
There is no uninstall command; removal is manual. There is also no hook script to delete — the hook runs from the repo checkout.
-
Remove the
SessionStartentry whose command referencesscripts/hook/pibmo-context.pyfrom~/.claude/settings.json. Leave any other hooks in place. If Pibmo’s was the only entry, you may remove the now-emptySessionStartarray. -
Optionally delete
~/.claude/machines.json, the banner’s private name map. Nothing else reads it — the sync registry at{root}/machines.jsonis a different file and is untouched.
Removing the hook stops the banner but changes nothing about archiving; the scheduler unit keeps syncing on its schedule, and the doctor still answers the liveness question on demand.