Skip to content

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:

Terminal window
bash ~/code/pibmo-recall/scripts/maintenance/deprecate-sync-hooks.sh

Run 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:

PathRole
~/.claude/settings.jsonGains 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.jsonThe 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:00
Machine: moraba (5A2B9C1D…) [macOS] host=moraba.local
User: you
Folder: pibmo-recall (/Users/you/code/pibmo-recall)
Git: main @ fa29cca https://github.com/attilabalogh/pibmo-recall.git
Last: fix(sync): antigravity-ide is a data dir -- include it minus media/binaries
Archive: 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"
WarningMeaning
⚠ 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 runA 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.jsonThe 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.json falls 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:

LineSource
Time, User, FolderThe local clock, environment, and working directory.
MachineThe hardware UUID (ioreg, /etc/machine-id, or the Windows registry) mapped through ~/.claude/machines.json.
ProjectThe name field of the nearest package.json, pyproject.toml, Cargo.toml, or go.mod.
Git, Lastgit run against the working directory: branch, short SHA, dirty flag, origin URL, and the last commit subject.
ArchiveThe scheduler unit or crontab line, {root}/machines.json, and the sync heartbeat in {root}/{machine}/pibmo.db.
SessionThe 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:

Terminal window
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:

Terminal window
python3 ~/code/pibmo-recall/scripts/hook/pibmo-context.py < /dev/null
echo "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.

  1. Remove the SessionStart entry whose command references scripts/hook/pibmo-context.py from ~/.claude/settings.json. Leave any other hooks in place. If Pibmo’s was the only entry, you may remove the now-empty SessionStart array.

  2. Optionally delete ~/.claude/machines.json, the banner’s private name map. Nothing else reads it — the sync registry at {root}/machines.json is 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.