Security posture
This document describes what Reasonbook does with your data, credentials, and network — plainly and truthfully. Every claim below names the source it rests on — a file in the source repository and, where it helps, the function or constant to look for — so you can verify it. (The npm package ships compiled code, so the links point at the repository.) If something here does not match the code, that is a bug in this document — please report it.
Reasonbook is a local-first MCP server. The default, fully-supported mode is: a stdio server running on your machine, reading and writing a single SQLite file in your project. Nothing listens on a network port. Ordinary local-only operations do not contact the dashboard. Dashboard-backed tool calls can, and startup may conditionally recover a missing project key when a registered project and usable stored user credential make that recovery possible. Authentication bootstrap can use the baked default host without an environment setting.
MCP tool annotations
Every published tool has a human-readable title and all four MCP effect hints. The explicit action inventory in tool-effects.ts is checked against the published actions and the security read/write taxonomy. Hints describe the union of every action's effects and the common MCP dispatch effects, following OpenAI's annotation definitions. Deletion, overwriting content or status, cancellation, revoking access and sending messages count as destructive. An undo path does not turn an overwrite into an additive operation.
All current tools advertise readOnlyHint: false, destructiveHint: true and
idempotentHint: false. Every MCP call, including a read or refusal, can append local telemetry;
the same append expires old monthly telemetry files. That deletion counts conservatively as
destructive, with no exemption for operational logs. Opening a project can also let the configured
upload scheduler pay upload debt from an earlier write. A configured upload may therefore follow
a read; its destination is the user's bounded dashboard account.
Only rbook_message and rbook_sprint advertise openWorldHint: true, because messaging and sprint
carry_forward can send to other people's projects. Their hints apply to every action, including
list and show. Access to the user's own bounded dashboard account alone does not make a tool
open-world.
These hints are different from action-taxonomy.ts, which governs a review agent's access to project records. A security-classified read can leave the record unchanged while writing telemetry outside the store. Tests check prepared store and registry snapshot hashes for enumerated actions with no action-owned mutation; separate tests exercise telemetry append and expiration through the registered MCP boundary. Those fixtures do not prove every argument combination, legacy migration or remote server behavior; effect labels also require code review.
Codex Auto mode can require approval for every current tool. These annotations do not promise fewer prompts or a passed plugin-directory review. Reducing approvals requires separately scoped runtime or tool-surface changes, rather than inaccurate hints.
Reporting a vulnerability
Please report security issues via the GitHub issue tracker (github.com/kneelinghorse/reasonbook/issues). For a sensitive report you would rather not file publicly, open a minimal issue asking for a private channel and we will follow up. There is no bug-bounty program.
What listens on the network
Nothing. Reasonbook is a stdio MCP server (the reasonbook bin → dist/bin.js, package.json, which
starts the server from dist/index.js when given no verb). It speaks JSON-RPC over stdin/stdout to its
MCP host and opens no listening socket. The same bin's CLI verbs (reasonbook hook …) run once and
exit; they open no socket either.
A previous release also shipped an HTTP transport bin (cmos-mcp-http) that bound a port with
Access-Control-Allow-Origin: *, no authentication, and full read-write access to every registered
store. It has been removed — source, bin, export, script, docs, and PM2 config are all deleted
(see the [2.0.0] → Removed entry in CHANGELOG.md). There is no unauthenticated
network surface.
Outbound network
Reasonbook runtime and plugin installation make outbound requests in these situations:
-
The dashboard, once you have a credential, or when you ask for it. The URL resolves from
RBOOK_DASHBOARD_URL, then the bakedhttps://reasonbook.aidefault (DEFAULT_DASHBOARD_URLin dashboard-client.ts).- Sign-in.
rbook_auth(action="login")orlogin_initcontacts that host even when no URL variable or credential exists.login_completepolls only when given adeviceCode; without one it returns a localMISSING_PARAMETERand sends nothing. - Registered-project writes and explicit completion calls start background uploads.
rbook_session(action="complete")andrbook_sprint(action="complete")start this best-effort work when a nonempty stored user-scoped key,RBOOK_DASHBOARD_API_KEY, or the completeRBOOK_DASHBOARD_USER/RBOOK_DASHBOARD_PASSWORDpair exists. A project key alone does not open that gate. The primary path uploads a consistent snapshot of the whole SQLite file, including pending proposal drafts and any approval words already copied into decision records. It may first register the project and save its project key. Completion success does not confirm upload success: this work is asynchronous. For a registered project, an MCP tool call that actually writes the store also schedules an upload, due after 5 quiet minutes or 30 minutes of continuous writes. The next poll starts it within 60 seconds when no upload lease or failure backoff blocks it; transfer time is additional. A running server must have opened the project. An owed upload survives process exit in the per-user registry, and concurrent servers share one lease per project. Only explicit closes may register a new project. Upload results and failures appear inrbook_statusandrbook_review; HTTP 401, 402 or 403 pauses automatic retries until an explicit close succeeds. Without a project slug an explicit checkpoint can fall back to event replay; a registered project's failed file upload has no replay fallback in that cycle, and a later checkpoint retries. Implicit process-exit closes do not upload; explicitly completing an implicit session through the tool does. SetRBOOK_CHECKPOINT_SYNC=offto disable these automatic upload paths (triggerCheckpointBackfillin checkpoint-backfill.ts). - Calls you make. Messaging, status and sync health,
rbook_dbsync actions (backfill, pull, clone, purge), sprint carry-forward, and the onboard and review summaries of messages. - Startup key recovery, when a registered project and a usable user-scoped credential exist but the project's key is missing.
- Shared-store propagation. Credentialed collab stores lock, pull and push around mission
transitions, sprint status changes/completion, and project-identity name changes
(sync-locks.ts).
RBOOK_CHECKPOINT_SYNC=offdoes not disable this propagation or explicit sync calls.
Without a credential, ordinary local work sends nothing: the baked address alone does not make any tool upload project data.
- Sign-in.
-
HuggingFace (
huggingface.co), only when the optional@xenova/transformerspackage is installed next to Reasonbook. The default install does not include it, and then Reasonbook never contacts HuggingFace: retrieval is keyword-only andrbook_db(action="health")reports semantic search off. With the package installed, the first write that records an embedding (a decision, learning or mission) or the first search downloads the embedding modelXenova/all-MiniLM-L6-v2(~25 MB, embedding-pipeline.ts), which is then cached locally. You can force embedding-model loading to remain offline withRBOOK_OFFLINE_EMBEDDINGS=1(and optionally a pre-seededRBOOK_MODEL_CACHE_DIR): the loader setsenv.allowRemoteModels=falsebefore loading (transformers-offline-env.ts), and if the model is not present locally the vector arm degrades to keyword-only retrieval instead of blocking on a fetch (getEmbedderin embedding-pipeline.ts). The token counter's tokenizer (Xenova/claude-tokenizer) loads only when something counts tokens, and no tool does. A local-forever install never hard-requires a network fetch. -
Plugin installation. A cold plugin launch runs npm to install the plugin's exact Reasonbook version and its dependencies. This contacts the configured npm registry; dependency install scripts may also download native packages from their suppliers. Until that install is ready, a command run through the plugin (not a hook) runs the same exact version with
npx -y --prefer-offline, which contacts the registry too unless npx's cache already holds it; in Codex, the plugin's server answers that Reasonbook is installing rather than starting through npx (run.mjs). This installation is separate from dashboard sync and does not upload the project database. The launcher validates package identity, CLI version and native SQLite before publishing the install (install-worker.mjs).pluginServer: "off"disables the plugin's MCP server before package discovery or installation; enabled developer hooks can still need their runtime.
Authentication model
Dashboard authentication is optional. Device-code bootstrap uses the effective dashboard URL
described above and does not require an existing credential. After bootstrap, credential-bearing
project clients are constructed by DashboardClient.fromEnvForProject()
(dashboard-client.ts); the selected credential source is
surfaced as an authTier (the AuthTier type and deriveAuthTier in
auth-state.ts):
device-code(preferred). RFC 8628 device-code flow viarbook_auth(action="login_init")+login_complete. Mints user-scoped and project-scopedcmk_keys stored locally.legacy-env. ARBOOK_DASHBOARD_API_KEYenvironment variable (RBOOK_DASHBOARD_API_KEY_ENVin dashboard-client.ts). Kept as a CI/script fallback; the server emits a one-time[WARN]nudging migration to device-code (warnLegacyAuth, same file).password-fallback. Email + password login. Also emits the migration[WARN].none. No stored credential — ordinary local work remains local and credential-requiring dashboard operations refuse gracefully. Credential bootstrap remains available:login/login_initrequire no existing key and may contact the baked dashboard host.
Data & credentials at rest
- Project data lives in one SQLite file,
rbook/db/rbook.sqlite, inside your project. It is not encrypted at rest (it is an ordinary SQLite database on your disk, with your filesystem's permissions). - Credentials live at
<configDir>/credentials.json, whereconfigDirdefaults to~/.config/rbookand honorsRBOOK_CONFIG_DIR(RBOOK_CONFIG_DIR_ENVin credential-store.ts). The file is written atomically with0600permissions (writeFileAtomic(…, { mode: 0o600 }), same file). - The
cmk_keys in that file are stored in plaintext — there is no encryption at rest and we make no such claim (the key fields in credential-store.ts are documented "Plaintextcmk_…key"). The protection is filesystem permissions (0600), not cryptography. Treatcredentials.jsonlike an SSH private key.
Backups & deletion — the honest reality
- Database snapshots are copies of the SQLite file under
rbook/db/snapshots/.rbook_db(action="snapshot")takes one on demand.rbook_sprint(action="complete")attempts snapshots before and after closing; a snapshot failure warns without blocking the close.rbook_db(action="prune_snapshots")requires a backup before applying a nonempty selection.rbook_db(action="restore")requires a copy of the live database underrbook/db/snapshots/pre-restore/before replacing it. These are the automatic backup paths. - Retention deletes.
RBOOK_MAX_SNAPSHOTS(default 50,resolveMaxSnapshotsin rbook-db-snapshot.ts) caps how many snapshots are kept: taking one deletes the oldest beyond the cap, automatic ones included. - The environment variables
RBOOK_AUTO_SNAPSHOT,RBOOK_SNAPSHOT_RETENTION_DAYS, andDB_PATHappear in older docs but are vestigial — no code reads them. Do not rely on them. - There is no
deleted_atsoft-delete net on the main store. Decisions and learnings carry a status (active/superseded/archived/stale), butrbook_db(action="restore")replaces the whole database, andrbook_db(action="purge")deletes this project's data from the dashboard mirror. A restore's pre-restore copy is not listed or accepted by restore-by-ID: keep its returned path for manual filesystem recovery with all writers stopped. That subdirectory is also outside the top-level snapshot retention count (rbook-db-restore.ts). - Snapshot content can be emptied.
rbook_db(action="prune_snapshots")is a dry run unlessconfirm=true; applied, it empties the content of context-snapshot copies Reasonbook wrote on its own (rows, ids, references and events stay) after taking a database snapshot.
Untrusted / foreign content
Text that Reasonbook did not author locally — inbound message bodies and summaries, project directory
descriptions, and decision/learning rows synced from other projects — is treated as data, not
instructions. It is rendered inside a source-labeled, self-escaping "untrusted" fence and carries an
additive {source, trust:"foreign"} descriptor
(provenance-frame.ts), applied across the message list,
onboarding, directory, and cross-store/pull-merged decision & learning renders. The rbook_message and
rbook_agent_onboard tool descriptions state this contract to the calling agent.
-
Decision & learning read surfaces framed: after a
rbook_db pull, the localstrategic_decisions/learningstables can hold rows authored in another project.project_idis derived read-time (no migration; column-presence guarded so ancient stores degrade toNULLand render bare, never throw) and a foreign decision or learning row — itsproject_id≠ the resolved local project — renders inside the untrusted fence, while local rows stay bare, at every surface that renders such rows:- the retrieval/search reads: mission-start "relevant decisions"
(relevance-surfacing.ts →
rbook-mission-start.ts, decision text and evidence),
rbook_context(action="search"),rbook_decisions(action="search"),rbook_learnings(action="search")(threaded through the retriever'sRankedResultand the two direct-SELECT search paths); - the aggregate/digest reads:
rbook_context(action="view")(full + compact),rbook_agent_onboard"Recent Decisions", and therbook_reviewdigest's recent-decisions.
This closes the former mission-start "relevant decisions" limitation for decision/learning content.
- the retrieval/search reads: mission-start "relevant decisions"
(relevance-surfacing.ts →
rbook-mission-start.ts, decision text and evidence),
-
MISSION / SPRINT / SESSION read surfaces framed: the same pull-merge path stamps a foreign
project_idonto pulledmissions/sprints/sessionsrows. Their name / objective / context / title / focus / summary fields are now derived read-time (same column-presence PRAGMA guard, so ancient stores degrade toNULLand render bare, never throw) and a foreign row — itsproject_id≠ the resolved local project — renders inside the untrusted fence while local rows stay bare, at every surface that renders such rows:rbook_agent_onboardcurrent-sprint header, active-session, and pending & blocked missions;rbook_mission(action="list")name/objective,rbook_mission(action="show")name/title/focus (inline) + objective/context/success_criteria/deliverables (block);rbook_mission(action="status")local work-queue (In Progress/Current/Queued/Blocked names + objectives + sprint title/focus) andacrossProjects=trueportfolio mission names (foreign fenced, the local project's own rows bare — the[proj:X]tag is metadata, not a trust boundary);- the
rbook_reviewdigest sprint title/focus, portfolio mission names, and theNext:recommendation (a foreign referenced mission renders id-only so its name never lands unfenced in the ≤4KB digest); - the
rbook_agent_onboardsuggested actions and therbook_reviewpromoted next_actions they feed — a foreign mission/session referenced by a "continue/start/resolve/complete" action renders id-only (name/title dropped) rather than fenced, keeping the byte-capped digest clean; rbook_session(action="list")title/summary andrbook_session(action="search")title + matched snippets.
This closes the former foreign MISSION/SPRINT/SESSION limitation; the decision/learning sweep above and this row-type sweep together frame every local-store read surface that can carry a pull-merged row. Scope boundary (ratified): the framed field set is name / objective / context / title / focus / summary (+ success_criteria / deliverables on
mission show). Missionnotesandreference_docsare not framed — they are operator-authored operational metadata (a blocker reason, a doc URI), rendered on a narrow set of surfaces, and were deliberately left out of the sweep; revisit if a real cross-owner share makes them an injection vector. -
The separate content sanitizer (content-sanitizer.ts) guards Reasonbook's own write paths against a specific tool-call-marshalling corruption; it is not the inbound-rendering mechanism above.
Dependency posture
Since 3.2.0 a consumer install carries no embedding stack. Measured on packed tarballs installed with
npm install --omit=dev into an empty project: 3.1.0's install was 305.7 MB and npm audit reported
1 critical and 5 high advisories, all through @xenova/transformers (protobufjs, onnx-proto,
onnxruntime-web, sharp); the 3.2.0 release audit reported none. A fresh macOS arm64 install checked on 2026-10-09
uses 45,972 KiB under node_modules by du -sk (44.9 MiB); allocation varies by platform
and dependency resolution. @xenova/transformers is an
optional peer dependency. If you install it for semantic search, its chain carries those advisories
into your tree, and this package's overrides cannot reach a dependent's tree: pin protobufjs to
^7 in your own overrides. In this repository the override still pins it for the development tree
(where the package is a dev dependency), guarded by
tests/release/dependency-overrides.test.ts.
On 2026-10-11, npm audit --omit=dev --json reported zero vulnerable package entries. The full
development installation's npm audit --json reported 2 affected package entries, both high,
counting Object.keys(vulnerabilities) and the report's severity totals; an entry can be flagged
only because a package it depends on is. Both come from three sharp advisories
(GHSA-f88m-g3jw-g9cj, GHSA-rgj7-g3m4-5g8c, GHSA-wq5f-xc86-pv6w) on one path,
@xenova/[email protected] → sharp: the optional embedding peer, a development dependency here.
They remain because npm's only offered fix downgrades @xenova/transformers to 1.4.2, and the
package reaches only people who install it for semantic search.
The same development tree reported 40 entries (9 moderate, 30 high, 1 critical) earlier the same
day and on 2026-10-10. Jest 30.5.2 and lint-staged 16.4.0 removed micromatch and braces, which
has no fixed release; handlebars 4.7.10, through ts-jest, cleared the critical. Two development
overrides cleared the rest: @istanbuljs/load-nyc-config gets js-yaml 4, dropping argparse 1
and sprintf-js, which has no fixed release either, and uuid is pinned to ^11.1.1 across the
development tree, where its only users are jest-cucumber (asking for ^10) and @cucumber/messages
(asking for exactly 9.0.1). Overrides apply only to this repository's own install, never to a
consumer's, and
tests/release/dependency-overrides.test.ts
fails if any of these advisories comes back. Developing in this repository needs Node.js 20.17 or
newer (lint-staged 16.4); the published package runs on Node.js 20 or newer. A consumer that
installs the optional embedding peer must audit that resulting installation separately.
Sanctioned deployment shape
Recommended: one project-local stdio server per project. Launch Reasonbook from the project directory
(or let your MCP host advertise the project via roots/list) so attribution resolves to the right
project. Sender/attribution resolution never consults RBOOK_PROJECT_ROOT at tool-dispatch time — that
env var is retained only as a bootstrap hint so the server can find its own .env (the module note
in sender-context.ts).
The one topology to avoid: a single global MCP entry that pins RBOOK_PROJECT_ROOT to one repo
while you work across several registered projects. That configuration ties .env bootstrap — and
any dashboard key that .env holds — to one repo that every sibling session shares. The server emits
a startup [WARN] when it detects exactly this — RBOOK_PROJECT_ROOT pinned and more than one
project registered (evaluateStartupTopology in index.ts). If you see that warning,
prefer a project-local server or pass projectRoot explicitly per call.
Credentials belong in ~/.config/rbook, not a repo .env. Keeping cmk_ keys out of any
repository avoids committing or mirroring them. The publish/mirror tooling additionally fails hard if
a real .env (anything but .env.template) ever reaches the public tree (the leak guard in
scripts/mirror-to-public.sh).
Read-only review agents (the review deployment)
Reasonbook ships a fail-closed read-only mode for agents that should never mutate your store — e.g. a
code-review agent. When RBOOK_AGENT_ROLE=review is set, a dispatch-layer guard
(read-only-agent-guard.ts, classifying every action via the
fail-closed action-taxonomy.ts) hard-rejects every write-classified
tool call before any database is opened and is a strict no-op when the env is unset.
To close the other data-loss vector — a review agent running git reset/stash/clean/etc. — the
repo ships a PreToolUse hook (scripts/hooks/block-git-mutations.sh)
that rejects destructive git commands. It is role-gated: a strict no-op unless
RBOOK_AGENT_ROLE=review, so it is safe to wire into any settings. To run a review deployment, use a
separate Claude Code / MCP host instance and apply
scripts/hooks/review-agent.settings.json (copy it to that
instance's .claude/settings.json) — it sets RBOOK_AGENT_ROLE=review and wires the hook, activating
both guards together.
Reads never write the record, under any role. Since 3.3.0 a read-classified call (the
READ_ONLY_ACTIONS list in src/tools/rbook/action-taxonomy.ts)
changes no row: no record status, no owner or address, no seeded identity, no schema label, no rebuilt
search index. Each repair that rewrites a value asks callMayWrite() in
src/tools/rbook/tool-call-context.ts first. Through 3.2.0 the
session opener marked old decisions and learnings stale on every review, the review role included. A
migration may still run on a read when it is DDL, the filling of a table or column that same call
added, and the new completion marker it writes under its own key when it ran that DDL
(ensureExternalFts and ensureVectorStorage in
src/tools/rbook/schema-migrations.ts). Store upkeep that
rewrites existing values (restoring rows an older Reasonbook marked stale, a pending master_context
migration, rebuilding a search index out of step with its table) runs only at a write, at a server
process's first write to a store (runFirstWriteMaintenance in
src/tools/rbook/first-write-maintenance.ts), and the
write's answer says what it did. A read that meets an index out of step says so on its answer, and the
server that read it rebuilds it at its next write. Repairs made on a write's way (an address heal, a
seeded identity row) are not that write (asLazyRepair): onboard, which makes no status write, never
starts the upkeep. Whoami and the startup lines write nothing at all; they resolve with the address the
next write would store. tests/tools/rbook/reads-never-write.test.ts
calls every read-classified action against stores seeded so each old write was possible, in both
roles, and fails on any committed row change.
Codex input boundary. The developer plugin wires SessionStart, UserPromptSubmit, Stop,
PreCompact and SessionEnd. Its hook parser consumes session_id, cwd, prompt, and
last_assistant_message; Codex's turn_id becomes the shared prompt_id. The shared parser also
accepts source and reason. The prompt can be searched for recall and observed for draft approval;
the latest assistant reply can produce the draft and its bounded excerpt described below. Codex
transcript_path is discarded before the shared parser, so Reasonbook does not scan a Codex transcript
as if it were a Claude transcript. Other stdin fields are ignored
(hook-adapters.ts,
core.ts). The chat plugin has no hooks.
For MCP calls, Reasonbook advertises codex/sandbox-state-meta and reads only the presence and object
shape of _meta["x-codex-turn-metadata"], the nonempty _meta.sessionId, and
_meta["codex/sandbox-state-meta"].sandboxCwd as a file:// URI. It ignores the marker's contents
and every other sandbox-state field; those fields are not copied into the Reasonbook request context.
An explicit projectRoot remains the first choice. Without it, malformed supplied metadata
refuses project resolution. The folder and conversation id live only for the request; background upload work
clears this context
(codex-request-context.ts).
The metadata is caller context, not authentication or proof of approval. Reasonbook uses a conversation
session key only when its folder resolves to the same physical store and that session has actual
hook state there. Otherwise it uses a process-owned session; merely supplying a session id cannot
manufacture hook approval. SessionEnd removes that session's hook evidence
(session-owner.ts,
draft-runtime.ts).
A chat-only direct decision can put observed approval words in context; Reasonbook cannot verify those
words and does not label that direct record as hook-approved.
Hook runtime files. The hook CLI keeps per-machine state under the config directory, never in a
project or the store (src/tools/rbook/harness-session.ts):
<configDir>/runtime/harness/<pid>.json links a harness process to its sessions, each by a
16-character SHA-256 prefix of the session id (harnessSessionHash), and holds the path of the
folder the conversation works in; <pid>.ended holds the prefix of the session that ended;
runtime/declined.json lists repositories whose user declined the init offer;
and runtime/fail-open.jsonl records hook verbs that gave up (the verb and a cause, never content;
recordFailOpen in src/cli/core.ts). No raw harness session id is written to the store, the
runtime files or a log. The hook verbs read the record without writing it (the digest is
rbook_review's read), except that session end closes the sessions the conversation opened, and that
drafts are kept in the store's proposals table (below). No hook verb registers a project in the
project graph or writes its identity.
Drafted records (3.3.0). When an agent ends a reply with a Would record: line, the Stop hook
stores it in the store's proposals table: the line, its kind, the hashed session, up to 600
characters of the reply before it, the links it named, and whether the session had read outside
content. The prompt hook writes only a decline or a draft's first-offer time there. Neither is a
record: the table is outside search, embeddings and sync events, and nothing in it becomes a
decision, constraint, learning or profile line without a rbook_decisions(action="record", fromDraft=...) call. The table is in rbook.sqlite, so a signed-in checkpoint upload carries it with
the rest of the store. The operator's reply to a draft is never written to the store by a hook.
The runtime uses one SQLite file per store under <configDir>/runtime/drafts/, keyed by a hash of
the project id and the store's native real path. It keeps:
- the newest 200 counted session-start times and each session's start time;
- offer counts per session and draft;
- the operator's reply, at most 1,000 characters, its class, the number of drafts it answered and the time it was received;
- the draft ids shown by the last reply, with their time;
- the draft ids in an approval window, its open/closed state and its time;
- transcript scan offsets and outside-content flags, keyed by hashes of transcript paths.
Session keys are 16-character hashes; raw session ids and transcript paths are not stored there
(src/tools/rbook/draft-runtime.ts).
Replies, shown sets and windows stop being usable after two hours. That is an expiry check, not a
background deletion timer: cross-store pruning runs only at a session start inside a Reasonbook project
with a harness session id, visits at most 50 runtime files, and skips files it cannot open. Binding
a new reply also prunes expired replies in that store. A successful session-end cleanup deletes
that session's runtime rows; the bounded start-time history remains. A crash followed by no such
start or cleanup can leave expired data on disk. Only a fromDraft record copies a reply into the
decision it approves, as that decision's approval_words. Under RBOOK_AGENT_ROLE=review session
starts do not prune or count toward draft expiry, and the hooks store, bind and decline nothing
(src/cli/drafts-hook.ts).
The plugin also keeps hashed source-election and turn receipts under runtime/hooks/, and
completed first-prompt receipts plus delivered typed IDs under runtime/recall/. These receipts
survive session end so duplicate adapters or resumed conversations do not replay completed work.
Transient runtime/lifecycle/ files hold the initial Git commit, dirty paths and compact marker;
successful session end removes only that conversation's transient files. Git summaries describe
repository observations, not agent authorship. The Claude plugin installer stores an exact
runtime/version marker with its package in CLAUDE_PLUGIN_DATA and an installation pointer under
the config directory. The Codex developer hooks and both Codex plugin servers instead share
<configDir>/plugins, with an exact version, platform, architecture and Node ABI in the install
identity. The developer entry forwards an optional absolute RBOOK_CONFIG_DIR; empty or relative
values are refused. The portable chat loader cannot forward that optional variable, so its supported
shared root is the default ~/.config/rbook. Both entries use the same installation validation. pluginServer: off is checked before package discovery or installation and exposes
no tools or instructions. None of this external runtime state is uploaded with the project store.
The operator profile. <configDir>/profile.md holds the operator's working preferences, for
every project. It lives outside every repository and every store, so it is never committed and never
uploaded with the store. reasonbook profile show prints it. Reasonbook's own write path (addProfileLine in
src/tools/rbook/operator-profile.ts)
adds a line only on an approved draft of kind profile, refuses a line past the 1,100-character cap,
and never cuts the profile to fit. That is a rule Reasonbook keeps, not a sandbox: it checks the approval
it is handed, not where the approval came from, and the profile is an ordinary file in the user's
config folder, which anything running as the user, an agent with file access included, can edit.
Honesty caveat (important). The machine-enforced read-only guarantee holds under this
separate-read-only-server deployment, where the review agent's MCP server is launched with
RBOOK_AGENT_ROLE=review. It does not automatically extend to in-session subagents spawned by a
normal build agent: those subagents share the parent's environment and MCP connection, so the parent's
(writable) server serves them. For read-only investigation within a build session, use a read-only
subagent type (e.g. the Explore agent) — that is a mitigation, not the machine-hard guarantee.
Last verified against the source for release 4.0.0.