Canonical, repo-first, CC-owned. This page holds the stable half of the chat ↔ Claude Code handoff — the standing rules, the documentation/single-writer rule, session hygiene, the operational procedure, the canonical glossary, and the stable REFERENCE facts. It is lifted out of the session-handoff (wiki id=15) so that every boot reads it once and every id=15 rewrite no longer re-transmits it.
Split of responsibility:
docs/session-protocol.md, wiki id synced normally): stable content. Written by CC via the repo (repo → wiki), like every other doc. This is the single canonical copy of the glossary and reference facts — CLAUDE.md and id=15 reference it, they do not duplicate it.The single-writer boundary is unchanged: this protocol page = CC (via repo); id=15 = chat.
STANDING RULES: (1) CC commits only CONFIRMED items (hard evidence; PROVISIONAL = don't commit). (2) ONE WRITER PER PAGE — id=15 = chat only; every other wiki page = CC only. (3) CC never requests master/admin creds via chat. (4) id=15 STAYS LEAN — once something has a commit hash it collapses to a one-line pointer there; full detail lives in the repo. Only content still in TO COMMIT (not yet committed) is kept at full length, because id=15 is then its only copy.
(5) STATE-STORE DEFAULT-EXECUTION RULE (standing law — Simon, 22 Jul): When CC reads the xzopia-state store (XS) and finds actionable TO_COMMIT items, CC actions them by default — it does not wait for per-item sign-off. Two OVERRIDES require Simon's explicit sign-off FIRST, and they apply regardless of any item marker: (a) the item is a production security-system change; or (b) it touches a live / keystone box — S2 (DR keystone), S4 (live MCP), S3 (live Comet BaaS). In case (a)/(b) the existing live-box discipline governs and overrides this default — halt, get explicit sign-off, backup/snapshot-first, and verify ground truth before touching (see "Server 4 — NEVER touch without backup first" and the S3/S2 discipline in CLAUDE.md, the REFERENCE server facts below, and the Sourced-Facts Rule). Auth/credentials are needed only if the item text explicitly states them — CC never invents an auth requirement and never requests master/admin creds via chat (rule 3). Marking convention: whoever stages a sensitive item (production-security, or touching S2/S3/S4) MUST prefix its body with ⚠️ SIGN-OFF REQUIRED. The absence of that marker does NOT waive (a)/(b) — the underlying live-box/production-security rule stands regardless; the marker is belt-and-braces, not the sole trigger. (Non-sensitive default work — repo docs, lessons, non-live-box code, wiki pages — CC just does and commits.)
DOCUMENTATION RULE (single writer — LESSON #109, 1a7c2ad): Chat is the SOLE writer of id=15. CC never writes it — CC posts a === id=15 UPDATES TO APPLY === block in BOTH its cc-report AND its final chat message (identical), and chat applies it. Chat CONSUMES the block by READING the CC report page by default (Simon says "apply from id=XX" → chat fetches + extracts + applies) — leaner than pasting. Fallbacks: (a) if the report omits the block, chat asks for the chat copy; (b) if Wiki.js MCP is down, paste is the manual override. CC owns all other wiki pages (cc-reports, cc-prompts, id=43 index) + the repo. Repo-worthy items go under "TO COMMIT (Claude Code)". Production/client op-logs → BookStack, not id=15.
SESSION HYGIENE (new-chat policy): Start a FRESH chat at each natural work-package boundary (a completed CC task + applied UPDATES block), or whenever responses feel slow (that's chat context weight, not the wiki — pruning id=15 won't fix it, a new chat will). Always safe: a new chat reads the lean id=15 (boot line) + this protocol + the repo and is fully oriented — nothing lives only in a chat's context that isn't in id=15 or the repo. Rule of thumb: after several big id=15 writes or a large tool-output dump, cut to a new chat at the next pause.
WIKI.JS MCP: intermittently drops — keepalive/auto-restart on S4 pending (79a82c5 promoted the fix; verify).
SOURCED-FACTS RULE (standing law — Simon 10 Jul; LESSON #126): No design, plan, or runbook may assert a fact about a live system that has not been read from that system. Every such fact carries its source — the command run, the page observed, the file read, the date. Facts without a source are marked ASSUMPTION and must be verified before anything is built on them.
b2: scheme · Cowork WSL/UNC paths · Power-Automate 1:1-chat · CIPP invite tenant-selector · restic 0.12.1 on S1) — each catchable by a single command or a single look, none by review. Supersedes the "chat-drafted runbooks need a run-through" candidate (LESSON #125) — that is a special case of this.SECRETS & CREDENTIAL HANDLING (standing law — Simon 24 Jul; canonical procedure docs/security/secrets-handling.md): Secret values never appear in any conversational interface — not claude.ai chat, not CC's own terminal session. No exceptions, regardless of who asks or why. Vaultwarden is the source of truth; if a secret isn't there, retrieve/reset it at the originating system's own console, not via chat or CC. On-box storage is root-owned 0600 env-style KEY=value at /etc/<service>/<purpose>.env, alongside the service's own config (precedent: /etc/cometd/license.env, /etc/cometd/api-admin.env, /etc/xzopia-backup/credentials).
0600 root-owned file itself where that's a trivial, non-disruptive touch (pre-change backup first on a live box); (c) commit a no-secret .template to the repo documenting the shape (precedent: scripts/server3/*.template).read -rsp + printf | sudo tee, or sudo nano) so it never touches shell history, a chat window, or CC's conversational input — then confirms by path only ("X is on the box"), never the value, and without re-explaining the mechanism.sudo bash -c 'source <path> && …') — never has it typed into a turn, never echoes it back.The handoff process itself, codified so both chat and CC drive it mechanically.
xzopia-state store (XS) — the live task queue (MCP state on S4:3013; read-only snapshot wiki id=157). Read its TO_COMMIT / AGENDA / SIMON_ACTIONS sections for open items not yet reflected in SP/SH — items can be staged in the DB only (a plain "read SP/SH" will miss them). XS is the authoritative working list: CC actions and closes CC-format items there autonomously (do the work, commit, then version-checked complete_item); any item needing Simon's sign-off says so inline.ListTenants finding; 8 Jul report id=88 re-read fully and caught its own same-day DR-runbook commit (c28a312) had gone stale, re-shipping the current version; 8 Jul a Cowork scheduled task (a different Claude surface) independently noticed it was reasoning from id=15's description of the DR runbook rather than the real file. Read the whole current page before forming conclusions — and more broadly, verify against the primary source, not a secondhand summary (id=15's TO COMMIT can be stale: items already committed but not yet reconciled out). See LESSONS #119 / #124.=== id=15 UPDATES TO APPLY === block.wiki_patch_page(id, old_str, new_str). old_str must occur exactly once (0 or >1 = rejected, no write). Fast, ~90% cheaper than a full rewrite, and no full-page clobber risk.—), or ellipses (…) → wiki_update_page full rewrite. Byte-exact old_str matching fails on those special characters, so patch is the wrong tool for spans that carry them; rewrite the whole page instead.The per-task session record is a DATED cc-report wiki page at xzopia/cc-reports/<YYYY-MM-DD>-<topic-slug> (date first — see the naming convention below), written by CC after each task (what was done · findings incl. surprises/risks · exact changes as diffs · blockers · next action) — mechanics in CLAUDE.md "CC ↔ Chat Communication via Wiki". These, plus the committed repo and the volatile id=15, are the running record. There is no longer a narrative per-session log kept by hand.
Where each thing lives now:
LESSONS_LEARNED.md (numbered lessons), PROJECT_STATUS.md (roadmap + current reference tables), runbooks, code.PROJECT_STATUS.md is a roadmap + reference-table doc, NOT a running session log. Its narrative "Session Log" section stopped at Session 19 (22–23 June 2026) when the process changed; its reference tables (server architecture / services / DNS) are kept current, but per-session narrative moved to dated cc-reports. DECIDED (Simon, 19 Jul 2026): dated cc-reports are the permanent replacement — the PROJECT_STATUS.md "Session Log" section is formally RETIRED (no July→ backfill; historical entries through Session 19 preserved but not extended). PROJECT_STATUS.md remains the roadmap + current reference tables only. A superseded banner marks the retired section in that file.
How it evolved (real dates, from git + wiki history — 19 Jul audit, cc-report doc-process-timeline-2026-07-19):
91b9199 (23 Jun).f01caa7): id=15 had grown large/slow, so the stable half was extracted out of id=15 into this page ("id=15 split"), and dated cc-reports became the standard per-task record — both to keep id=15 lean. (id=15 hit 330K+ again on 19 Jul — the size pressure recurs; cut to a fresh chat and keep writing dated cc-reports rather than growing id=15.)⚠️ cc-report page IDs are Wiki.js's GLOBAL page-ID counter (shared across every category — prompts, docs, cc-reports all draw from the same sequence). Never infer a report's age from its ID (other categories used IDs in between; id=129 is not "2 days old" because it's a high number). Order/date cc-reports by their path date (<YYYY-MM-DD>-…, date-first per the naming convention below) or createdAt, never the ID.
🏷 Wiki page naming convention (adopted 22 Jul 2026, Simon). Numeric Wiki.js page IDs are a global auto-increment shared across every page type — not meaningful, and not sortable by age (per the caveat above). Paths are the real naming layer. Apply going forward — existing pages are NOT migrated, new pages only:
xzopia/docs/<topic-slug> — no date, edited in place (e.g. session-handoff, this session-protocol page).xzopia/cc-reports/<YYYY-MM-DD>-<topic-slug> — date FIRST, so the page list sorts chronologically without opening anything. Supersedes the earlier date-last form (e.g. id15-trim-2026-07-22 → would now be 2026-07-22-id15-trim).xzopia/cc-prompts/<YYYY-MM-DD>-<topic-slug> — date first, same reason.xzopia/chat-prompts/<YYYY-MM-DD>-<topic-slug> — date first. Mirrors the CC-prompts pattern for the other direction: when a chat-originated instruction to CC warrants a permanent record (not just a chat-transcript mention that vanishes), CC stages it as a chat-prompt page. Adopted 22 Jul 2026 after two 22-Jul chat prompts (wiki naming convention, SH-glossary add) left no wiki record.wiki_get_page id=NN).Added 3 Jul for model-portability. This is the single canonical copy (CLAUDE.md and id=15 reference it).
=== id=15 UPDATES TO APPLY === (CC → chat, dual-emitted) · SIMON ACTIONS = human-gated queue · CONFIRMED/PROVISIONAL = commit-eligibility status · boot line = the paste-in orientation line at the bottom of id=15.Stable configuration/topology facts. CC updates these here (repo → wiki) as infrastructure changes; volatile task state stays in id=15.
BACKUP (fleet — wiki id=23/id=24; S2/S5 verified id=72, S1/S4 built id=73 378e32a pushed; DMS armed all 4; off-infra passphrase confirmed; Object-Lock cutover staged+pushed c23d488 id=74): bucket xzopia-backup (eu-central-003); repos server1/(02:00) server2/(01:00) server4/(02:30) server5/(01:30); creds /etc/xzopia-backup/credentials (export OBJECT_LOCK="on" on all 4 as of 6 Jul — flag-first cutover step done, verified forget-only branch selected); passphrase VW Backup → Restic Passphrase + off-infra copy; server key HAS deleteFiles; alerting msmtp/Mailgun EU + healthchecks.io DMS (S1 9d5914f9-…, S2 595973f7-…, S4 bf44bbb0-…, S5 52601f0b-…). Scripts scripts/serverN/restic-backup.sh (branch on OBJECT_LOCK). S6 = build at reimage. Object Lock cutover: ✅ COMPLETE + VERIFIED 6 Jul — OBJECT_LOCK=on all 4; B2 xzopia-backup governance lock (7-day default) ACTIVE. Verified: all 4 repos restic check clean + snapshots readable; S4 manual run wrote a new snapshot + ran forget-only (Object Lock mode: on — prune SKIPPED) successfully under the lock; server-key delete of a freshly-locked object DENIED (HTTP 401 — key has deleteFiles, lacks bypassGovernance) = governance ENFORCED. Governance-not-compliance = reversible via the bypassGovernance master key held in VW Backup → B2 Master Key (OFF servers). Space reclaim = planned privileged op (repo rotation / master-key bypass).
PATCH/REBOOT (runbook docs/runbooks/fleet-patch-reboot-policy.md, e5eb654 PUSHED, id=75): u-u WORKING all of S1/S2/S4 (none inert, log-proven). Livepatch ENROLLED + VERIFIED S1/S2/S4 (canonical-livepatch = checked / nothing-to-apply; running kernels S1 -179 / S2 -124 / S4 -134); 4/5 free personal seats (S6 at build); S3/S5 excluded. Pending kernel reboots → next ATTENDED window: S1 -179→-185 (out-of-hours), S2 -124→-134 (snapshot-first). Monthly attended staggered reboots; no unattended auto-reboot on live boxes; S6 never rebooted same slot as boxes it monitors. Exposed Pro token (pgrep leak 3 Jul) ASSESSED 6 Jul: free-personal tier not rotatable → monitor dashboard seats (residual watch, low impact — see id=15 SIMON ACTIONS; lesson #120).
DR / WORKSTATION REBUILD (runbook docs/runbooks/disaster-recovery.md): recovery procedure for (a) workstation/WSL loss, (b) any single server, (c) S2 (DR keystone). Source-of-truth hierarchy: GitHub repo → Vaultwarden (S2) → B2 Restic snapshots → off-infra SSH-key + Restic-passphrase copies. Off-infra Restic passphrase breaks the S2 circular dependency (Scenario C). Restic repo scheme is S3-compatible s3:https://s3.eu-central-003.backblazeb2.com/xzopia-backup/serverN, NOT native b2: (native fails "Account ID is empty" on this fleet's AWS_*-style credentials layout — CONFIRMED via drill 8 Jul). Off-infra-passphrase decrypt drill PASSED 8 Jul (from S4: 11 snapshots listed against the real S2 repo, off-infra copy only). Deeper restore+integrity drill PASSED 8 Jul (real restic restore 280 files/17.8 MiB + restic check --read-data-subset=5% clean + SQLite integrity_check ok). Drill host = S7, a LOCAL WSL2 VM (xzopia-s7-drill, Docker+restic, NOT Fasthosts, off between drills) built 8 Jul to close the "no safe container-spin-up host" gap (S4 has no Docker, S2 is live prod) — enables the Level-3 container-unlock proof. Cadence: quarterly, first drill 1st week Oct 2026 (then Jan/Apr/Jul/Oct); "drill overdue" reminder via a healthchecks.io DMS (Simon to create). Build script scripts/server7/build-s7-drill.sh. Includes what's-not-in-git recovery table + per-event sign-off checklist.
SERVER TOPOLOGY (corrected 2 Jul):
-185)-134, snapshot-first)2bef097); hardened Lynis 75; backup: LIVE (02:30, restore-proven 3 Jul, DMS armed); Livepatch ON; kernel currentxzopia-s7-drill, Ubuntu 24.04, Docker+restic) — NOT Fasthosts, not always-on, reached via wsl -d (no SSH/IP); quarterly DR restore-drill host, built 8 Jul, off between drills. Fleet skills (audit/backup/pentest/pre-flight) do not apply.S4 PORT MAP: 3001 NinjaOne · 3002 Xero · 3003 GIACOM · 3004 Wiki.js · 3005 M365-legacy(abandoned) · 3006 SentinelOne (S1-EDR) · 3007 TRMM · 3008 ITFlow · 3009 BookStack · 3010 TwentyCRM · 3011 M365Admin (+ send_mail) · 3012 CompaniesHouse (search_company, get_company_profile — IP-restricted to S4, added 18 Jul).
CIPP MCP (native connector — NOT S4-hosted, no port): CIPP's own MCP Server feature over OAuth; endpoint https://cippbun2i.azurewebsites.net/api/ExecMcp (token scope = literal App-ID-URI .../ExecMcp/.default, not api://{client-id}/.default). Sits outside the 12-connector S4 pattern. Design: docs/design/cipp-mcp.md. Known issues: BUG #1 (claude.ai-web-only, upstream anthropics/claude-ai-mcp#154 — cleared via Claude Code, informational); BUG #2 ListTenants malformed allOf schema (CONFIRMED, still live, KelvinTegelaar/CIPP#6297) — real fix = upstream enrichment-step schema correction (redeploy is a no-op; 10.6.0's generator still emits the bad schema — id=107). A CIPP Custom Role blocking ListTenants is defence-in-depth only, verified INEFFECTIVE (Blocked Endpoints blocks calls, not tools/list visibility — id=108). Interim lever = ExecMcp URL scoping (?tools=/?tags=) for CC's client_credentials use — breaks the claude.ai OAuth connector; id=110/111. Design: docs/design/cipp-mcp.md.
COMMS APP (Xzopia Automation): appId d7780cc1-247b-4aa7-827f-6ccca5b955e5 · app obj 61b4be6e-cfcc-4274-a1fc-bd6f84020343 · SP obj 652c074b-630b-4b5d-9e4a-732525063582 · tenant 0c49e13b-0b64-47af-aadd-cb13a99efd1f. Single-tenant, publisher-verified 1071553. App-only Mail.Send (LIVE via send_mail 2bef097) → policy group SendMail-Allowed → alerts@xzopiasecure.com. Secret in Vaultwarden + S4 .env. Delegated Teams scopes later (same app).
BOOKSTACK: Security(1) → SentinelOne(2): id=3 placeholder, id=4 O'Boyle (CLOSED 8 Jul), id=7 Daily SOP, id=11 Daily S1-EDR report. Engineer Queue(5) id=6 (12 items). Client Operations(8) → DHS(9) → id=10. Internal — Company Policies(16) → chapter id=17 (now incl. Xzopia's own Data Protection Policy, added 20 Jul — was the one missing sibling). NEW: Client Service Packs(book id=30) — searchable per-client policy/contract storage (distinct from Internal Policies + Client Operations), built 20 Jul: chapters Webottle Limited (id=31, 11 pages), Momentum Tax Group (id=44, 10 pages — closes the "10 client policy docs" gap from reports id=150/151), Templates for new-client onboarding (id=55, 9 pages, blank [bracketed]-field masters). BookStack MCP has FULL write access — create_book/create_chapter/create_page/update_page all confirmed working (supersedes any stale "BookStack MCP read-only" note). BookStack is text/markdown-only (no attachment upload) → binary/signed docs need Nextcloud (roadmap, Phase 4). (16 write tools.)
TRMM PILOT: agent DESKTOP-CC2U7F7. Policy id=12 tasks 22/23/24 ✅. Scripts 145-151 live; KB5034441 (147) pilot affected, -Apply deferred. (TRMM MCP 39 tools.)