The single first-run entry point that stands up the WHOLE Cos system, sequencing the four component setup skills in dependency order — setup-vault → guard-setup → mcp-bridge-setup → backup-recovery — then finishes with the automation step, installing the operator skill bundles into Cowork and creating the scheduled tasks the generated catalog lists. Use when setting up the chief of staff system, doing a first-run setup, onboarding a new machine, or asking for the full setup; also when you're ...
Install to Claude Code
npx -y skills add philipyaz/cos --skill cos-setup --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Cos Setup?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/philipyaz-cos-setup)More formats (shields.io, HTML) on the badges page.
---
name: cos-setup
description: The single first-run entry point that stands up the WHOLE Cos system, sequencing the four component setup skills in dependency order — setup-vault → guard-setup → mcp-bridge-setup → backup-recovery — then finishes with the automation step, installing the operator skill bundles into Cowork and creating the scheduled tasks the generated catalog lists. Use when setting up the chief of staff system, doing a first-run setup, onboarding a new machine, or asking for the full setup; also when you're unsure which component skill to run first and want the guided end-to-end runbook. Covers the three multi-device paths at Step 0.0 — a fresh hub, joining as a spoke (routes to spoke-setup), or promoting this machine to hub in a hub swap (routes to hub-handover).
allowed-tools: Bash, Read
---
# Cos — full first-run setup (orchestrator)
This is the **root runbook** that brings the whole system up on a fresh machine. It does not
re-implement the component skills — it **sequences** them in the one order that works, because
each step produces what the next one needs: the **vault** must exist before the MCP can point at
it and before backup has something to protect; the **guard model** must be configured before the
guard bridge can report a real classifier; **all the bridges + sidecars** must be wired before
backup can snapshot live, populated stores. Run the four sub-skills **in order**, stop at each
**CHECKPOINT**, and only advance when it passes. End with the **§ End-to-end verification**, then
finish with **Step 5 — operator automation**, which is what makes the system *run itself*.
Every shell step below begins with the loader line
`source "$(git rev-parse --show-toplevel)/config/load-config.sh"`, which exports `$REPO_ROOT`
(git-derived), `$BREW_PREFIX`, `$LAUNCH_AGENTS_DIR`, `$COWORK_CONFIG`, `$VAULT_DIR`, `$BOARD_URL`,
and the bridge/sidecar ports + URLs — use those instead of hardcoding paths, ports, or your
username. The only value still derived inline is `$U=$(id -u)`, which `launchctl` needs at runtime.
## Prerequisites checklist (gather these BEFORE step 1)
- [ ] **node + npm** (Homebrew) — bridges, vault Agent SDK, backup. `node -v`.
- [ ] **supergateway** — `npm install -g supergateway`. The stdio→HTTP bridge for Claude Code.
- [ ] **uv** (Homebrew: `brew install uv`) — runs the two Python sidecars (search `:8008`,
guard `:8009`); self-provisions each venv on first launch.
- [ ] **hf CLI** (`pip install -U huggingface_hub` / `brew install huggingface-cli`) **+ a Llama
license** — only if you want the real gated guard model (step 2). The heuristic-only fallback
needs none of this.
- [ ] **ANTHROPIC_API_KEY** (`sk-ant-…`) in **`config/secrets.env`** — the **vault** bridge embeds
the Agent SDK and makes outbound Anthropic calls; it is the only bridge that needs a key, and
the key goes in this one gitignored file (the launch wrapper loads it), never in the plist.
- [ ] **A PRIVATE GitHub backup repo** + a **recovery passphrase** — step 4. Encrypted, off-site.
The whole system runs on these ports — keep them free
(`lsof -nP -iTCP:<port> -sTCP:LISTEN`): **3000** board app · **8001** board · **8003** calendar ·
**8004** guard · **8005** vault (core bridges) · **8002** openwhispr · **8006** whatsapp · **8007**
nutrition · **8011** fitness · **8012** body (optional add-on bridges) · **8008** search · **8009**
guard · **8010** whatsapp-go (sidecars). The `8002` (openwhispr), `8006`/`8010` (WhatsApp), `8007`
(Nutrition & Chef), `8011` (Fitness), and `8012` (Body) ports are only needed if you run those
optional add-ons (Steps 3.4–3.8).
---
## The sequence
### Step 0.0 — first Cos machine, joining as a spoke, or taking over the hub role? (multi-device)
**Only want to VIEW the board from another device? You need NOTHING on that device — no spoke, no
runbook.** Run the **production** board on the hub (`cd board && npm run build && npm run start`, or
install the `boardapp` LaunchAgent — **not** `next dev`, whose on-demand compilation + lazy chunks
are unreliable through a reverse proxy: the page loads but opening a case can silently fail) and
expose it on the tailnet with `tailscale serve --bg 3000` (needs Tailscale HTTPS/MagicDNS). The
board is loopback-bound, so `tailscale serve` is the ONLY door in — and the URL is **portless**
(HTTPS 443): any tailnet device opens `https://<hub>.<tailnet>.ts.net` in a browser and gets the
**full read/write UI** (the browser writes through the same HTTP API). A **spoke** (below) is only
for running Claude/Cowork **agents** on the second machine — never needed just to view.
Otherwise, decide the **role** of this machine — there are **three** setup paths:
- **First Cos machine (a fresh HUB)** — this machine runs the state machine (board on :3000, its own
`cases.json`, sidecars, backups, the Cowork routines) and there is **no existing hub** to inherit
from. Continue with Step 0 below. `COS_DEVICE_ROLE` stays `hub` (the default). This is the path the
rest of this runbook documents.
- **Joining an existing Cos (a SPOKE)** — this machine is a stateless CLIENT of an existing hub: its
board-facing MCP wrappers point at the hub's `BOARD_URL`, and it has **no local store of its own**.
A spoke exists for **one** reason — to run Claude Code + Cowork **agents** against the hub through
**LOCAL stdio MCP** tools. It is required because **Cowork only accepts local stdio MCP servers**;
it can't consume the hub's remote HTTP MCP over the tailnet (a hard, validated fact), so a thin
local shim forwarding tool calls to the hub's `/api/*` is the only way. If you just want to *view*
the board here, stop — use a browser (top of this step); a browser can't give a local agent tools.
Then **SKIP Step 0 entirely** (there is no local store to seed — the board-seed copy that follows is
the documented seed-over-live-data footgun, and a spoke must never run it), skip the vault/guard-model
steps and Step 4 (backup is hub-only), and run the **`spoke-setup`** skill instead — it flips
`COS_DEVICE_ROLE=spoke`, points `BOARD_URL` at the hub, and wires only the board-facing wrappers.
You get the hub's join string (`cos-join://…`) from the hub's board **Devices** panel ("Add a device")
or `node scripts/join-blob.mjs` on the hub.
- **Joining but PROMOTING this machine to HUB (a hub swap)** — an existing hub is **stepping down** and
THIS machine becomes the new hub (the old one demotes to a spoke, or retires). This is neither a fresh
first-run nor a plain spoke join, so do **NOT** run this runbook straight through — the data-safety is
in the *order* of the swap. Run the **`hub-handover`** skill, which sequences the freeze → final-backup
→ cutover → demote ceremony and **reuses this runbook's component steps with two deliberate
substitutions**:
- **Step 0's seed becomes a RESTORE.** The new hub is hydrated from the old hub's newest **encrypted
snapshot** (`node "$REPO_ROOT/backup/restore.mjs" --device <old-hub-id> --apply`, with no board
running), never from the fixture — seeding would create a divergent empty store that the cutover
re-restore only has to overwrite. (`restore.mjs --list` on this machine names `<old-hub-id>`.)
- **Step 4's backup PRODUCER role is DEFERRED to cutover.** You still do Steps 0.5–3 (cos.env, vault,
guard model, bridges) and provision the **same recovery key** as the old hub, but you do **NOT**
install this machine's backup LaunchAgent until the handover cutover, where `backup.mjs --claim`
takes the `HUB.json` lease. Two producers racing that lease is exactly what the ceremony exists to
prevent — old hub off, new hub on, in that order.
Prereqs (`hub-handover` enforces them): both machines on the **same Tailscale tailnet**, the **same
recovery key** on both, and this machine's checkout at **`SCHEMA_VERSION` ≥ the store's schema** (the
migration's safe direction — otherwise the board fails closed with `SchemaAheadError` after restore).
The rest of this runbook is the **fresh HUB / first-machine** path.
### Step 0 — seed runtime stores (fresh public clone only)
- **What it does** — a fresh clone of the public repo ships WITHOUT real runtime data
(`board/data/cases.json`, `config/settings.json`, `guard/data/*.json` are gitignored). Seed the
board + config once from the shipped synthetic counterparts so everything boots:
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh"; cd "$REPO_ROOT"
cp tests/fixtures/board-seed.json board/data/cases.json # board store — OR skip: the board
# auto-creates an EMPTY store if absent
cp config/settings.example.json config/settings.json # then HAND-EDIT "principalEmail" to yours
```
`config/settings.json` holds board/app prefs; you must set `principalEmail` by hand (the copy
ships a placeholder — nothing auto-fills it). The next step (0.5) adds `config/cos.env` to the
`config/` picture: the machine-local paths/ports for the skills.
- **Skip** on the private dev machine where the real stores already exist.
- **CHECKPOINT** — `config/settings.json` exists with your `principalEmail` (and `board/data/cases.json`
exists, or you accept an empty board that the app creates on first run).
### Step 0.5 — generate `config/cos.env` (machine paths / ports for the skills)
- **What it does** — writes `config/cos.env`, the machine-local **public** config every skill
reads through the loader: the absolute Homebrew prefix, the `node`/`uv`/`supergateway` binary
paths, the LaunchAgents + Cowork config locations, the OpenWhispr store paths, the backup repo,
and all the ports (board `:3000`, bridges `:8001–8005`, sidecars `:8008/:8009`). It writes
**only** these public values — **no secrets**. The config split is deliberate, four files under
`config/`:
- **`cos.env`** — machine paths + ports for the **skills/setup** (this step). Public, gitignored.
- **`secrets.env`** — the `ANTHROPIC_API_KEY` (step 3). The loader does **not** source it; the
vault bridge's own launch wrapper (`launch.sh`) does.
- **`settings.json`** — board/app prefs incl. `principalEmail` (step 0).
- **`auto-sync.json`** — the ingest router's auto-sync switch.
- **Idempotent** — never overwrites an existing `cos.env` (delete the file to regenerate, e.g. if
you changed machines and `BREW_PREFIX` moved). `VAULT_NAME` is left blank here — **step 1
(setup-vault) fills it** once your vault exists.
- **Why before the vault** — from here on every shell step in this skill and the component skills
begins with the loader line `source "$(git rev-parse --show-toplevel)/config/load-config.sh"`,
which sources this file and exports `$REPO_ROOT`, `$BREW_PREFIX`, `$LAUNCH_AGENTS_DIR`,
`$COWORK_CONFIG`, `$OPENWHISPR_DB`, `$VAULT_DIR`, `$BOARD_URL`, the bridge/sidecar ports + URLs,
etc. (`$REPO_ROOT` is git-derived, never stored in the file.) Generate it first so every step
downstream resolves to your machine's real values.
- **Run** (the heredoc body must stay flush-left — `<<EOF` preserves leading whitespace, so do not
indent these lines or `cos.env` would gain stray spaces):
```bash
# --- Step 0.5 · Generate config/cos.env (machine paths / ports for skills) ---
# Idempotent: never overwrites an existing cos.env (delete the file to regenerate).
source "$(git rev-parse --show-toplevel)/config/load-config.sh" # sets $REPO_ROOT, $BREW_PREFIX, $HOME defaults
cd "$REPO_ROOT"
if [ -f config/cos.env ]; then
echo "config/cos.env exists — leaving it untouched (delete it to regenerate). Current values:"
grep -E '^[A-Z]' config/cos.env
[ -d "$BREW_PREFIX" ] || echo "WARNING: BREW_PREFIX '$BREW_PREFIX' not found — changed machines? delete config/cos.env and re-run."
else
tmp="$(mktemp)"
# Locate Claude Cowork Desktop's config file PER OS, and VERIFY its directory exists (= Cowork is
# installed there). Don't just assume the macOS path: on Windows it lives under %APPDATA%. If the
# dir is missing, keep the best-guess default but WARN — you MUST point COWORK_CONFIG at the real
# file before mcp-bridge-setup's Cowork step writes to it (gen-cowork-config.mjs will refuse a
# nonexistent dir). When in doubt, find it: `ls -d ~/Library/Application\ Support/Claude` (macOS) /
# `ls "$APPDATA/Claude"` (Windows Git Bash).
case "$(uname -s)" in
Darwin) COWORK_CONFIG="$HOME/Library/Application Support/Claude/claude_desktop_config.json" ;;
MINGW* | MSYS* | CYGWIN*) COWORK_CONFIG="${APPDATA:-$HOME/AppData/Roaming}/Claude/claude_desktop_config.json" ;;
*) COWORK_CONFIG="$HOME/.config/Claude/claude_desktop_config.json" ;;
esac
[ -d "$(dirname "$COWORK_CONFIG")" ] || echo "WARNING: Cowork config dir '$(dirname "$COWORK_CONFIG")' not found — is Claude Cowork Desktop installed? If it lives elsewhere, set COWORK_CONFIG in config/cos.env to the real path before running mcp-bridge-setup."
cat > "$tmp" <<EOF
# Cos — machine-local PUBLIC config (paths/ports for SKILLS + SETUP).
# Generated by cos-setup. Safe to edit. NO SECRETS (those live in config/secrets.env).
# Skills read this via: source "\$(git rev-parse --show-toplevel)/config/load-config.sh"
# ALWAYS QUOTE values (paths contain spaces). REPO_ROOT is git-derived, never stored here.
BREW_PREFIX="$BREW_PREFIX"
NODE_BIN="$BREW_PREFIX/bin/node"
UV_BIN="$BREW_PREFIX/bin/uv"
SUPERGATEWAY_BIN="$BREW_PREFIX/bin/supergateway"
LAUNCH_AGENTS_DIR="$HOME/Library/LaunchAgents"
COWORK_CONFIG="$COWORK_CONFIG"
# OpenWhispr voice add-on (optional; wired by openwhispr-mcp-setup) — paths to the desktop app's store.
OPENWHISPR_DB="$HOME/Library/Application Support/open-whispr/transcriptions.db"
OPENWHISPR_AUDIO_DIR="$HOME/Library/Application Support/open-whispr/audio"
BACKUP_REPO="$HOME/.cos-backups"
VAULT_NAME=""
BOARD_PORT="3000"
BOARD_BRIDGE_PORT="8001"
OPENWHISPR_BRIDGE_PORT="8002"
CALENDAR_BRIDGE_PORT="8003"
GUARD_BRIDGE_PORT="8004"
VAULT_BRIDGE_PORT="8005"
SEARCH_SIDECAR_PORT="8008"
GUARD_SIDECAR_PORT="8009"
# WhatsApp MCP add-on (optional; wired by whatsapp-mcp-setup). WHATSAPP_GO_PORT is the Go
# whatsmeow bridge sidecar (8010; whatsmeow's default 8080 is usually taken).
WHATSAPP_MCP_DIR="$HOME/Code/whatsapp-mcp"
WHATSAPP_MCP_BRIDGE_PORT="8006"
WHATSAPP_GO_PORT="8010"
# Nutrition & Chef add-on (optional; built-in in-repo mcp/nutrition-server, wired by
# nutrition-mcp-setup). NUTRITION_BRIDGE_PORT is the supergateway HTTP bridge for Claude Code;
# the add-on is gated per-board via Settings.addons, so naming the port only documents it.
NUTRITION_BRIDGE_PORT="8007"
# Fitness + Body add-ons (optional; in-repo mcp/fitness-server + mcp/body-server, wired by
# fitness-mcp-setup / body-mcp-setup — Steps 3.7 / 3.8). Same "documents the port" role as above.
FITNESS_BRIDGE_PORT="8011"
BODY_BRIDGE_PORT="8012"
EOF
mv "$tmp" config/cos.env
echo "Wrote config/cos.env — review it, then continue. setup-vault (Step 1) fills VAULT_NAME."
fi
```
- **CHECKPOINT** — `config/cos.env` exists, lists your real `BREW_PREFIX`, and sourcing the loader
exports the vars:
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh"
echo "$REPO_ROOT $BREW_PREFIX $LAUNCH_AGENTS_DIR" # all three populated for your machine
```
### Step 1 — setup-vault (FIRST: create the knowledge target)
- **What it does** — creates a **private vault instance** from the committed template
`$REPO_ROOT/vault/example-vault/` (`cp -R vault/example-vault vault/<name>`), records the slug as
**`VAULT_NAME`** in `config/cos.env` (the loader derives `$VAULT_DIR` from it, and the vault plist's
**`COS_VAULT_DIR`** regenerates from that via `scripts/gen-launchd.mjs` when step 3 installs it),
**registers it with Obsidian and records its unique vault ID** in
`config/settings.json` (so the board's `obsidian://` deep-links open THIS in-repo vault, not a
same-named copy elsewhere — STEP 3.5 of setup-vault), **folds it into the backup `SCOPE`**
(auto-derived from `VAULT_NAME` in `backup/config.mjs` — no hand-edit), and confirms it is
**gitignored** (the real vault holds PII and is NOT git-backed — its durability comes from step 4's
encrypted off-site backup; only `example-vault` is tracked).
- **Why FIRST** — the MCP needs a target to scope `COS_VAULT_DIR` to, and backup needs something
to protect. Nothing downstream works without the vault directory existing.
- **Prereq** — the committed template `$REPO_ROOT/vault/example-vault/` is present (it ships with
the repo). Confirm: `ls "$REPO_ROOT/vault/example-vault/"`.
- **Run** — invoke **`/setup-vault`** (or follow its steps): `cp -R vault/example-vault vault/<name>`,
record `VAULT_NAME="<name>"` in `config/cos.env` (backup `SCOPE` then auto-derives from it; the
vault plist's `COS_VAULT_DIR` regenerates from `$VAULT_DIR` when step 3 installs the bridge).
- **CHECKPOINT** — all three must hold before step 2 (`VAULT_DIR` resolves to
`$REPO_ROOT/vault/$VAULT_NAME` once setup-vault has set `VAULT_NAME` in `cos.env`):
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh"
ls "$VAULT_DIR" >/dev/null && echo "vault dir OK"
git -C "$REPO_ROOT" check-ignore "vault/$VAULT_NAME" && echo "gitignored OK" # prints the path = ignored
grep -q "vault/$VAULT_NAME" "$REPO_ROOT/backup/config.mjs" && echo "in backup SCOPE OK"
```
Plus, for board deep-links to open the right vault: `config/settings.json` may carry a 16-char
`obsidianVaultId` (setup-vault STEP 3.5). A **blank** id is non-fatal and now **self-heals** — the
board reads the id *through* from Obsidian's own registry (`vault-config.ts` realpath-matches
`obsidian.json`) the moment the vault has been Open-folder-as-vault'd, so the ↗ deep-link and the
/vault "Registered with Obsidian" check light up on the next **Refresh** without re-running
anything; the in-app vault preview works regardless. So only flag the case where the vault is in
**neither** place (no id AND never opened in Obsidian):
```sh
grep -o '"obsidianVaultId": *"[0-9a-f]\{16\}"' "$REPO_ROOT/config/settings.json" && echo "obsidianVaultId OK" \
|| echo "note: no obsidianVaultId in settings.json — fine IF you've opened vault/$VAULT_NAME in Obsidian (the board self-detects it); otherwise do File → Open Vault → Open folder as vault to enable the ↗ deep-link"
```
### Step 2 — guard-setup (the prompt-injection classifier model)
- **What it does** — configures the **guard classifier model** the sidecar (`guard/sidecar.py`,
`:8009`) runs: picks a preset (default the **gated Meta `Llama-Prompt-Guard-2-86M`**), accepts the
Llama license + authenticates with `hf`, prefetches the model, sets
`COS_GUARD_MODEL`/`COS_GUARD_THRESHOLD`/`COS_GUARD_CLASSIFIER` in `config/cos.env`, installs the
guardsvc launchd plist (generated from `guard/guardsvc.service.json` by `scripts/gen-launchd.mjs`
— see `mcp/CLAUDE.md`), and verifies the sidecar reports the **real model**, not the
`heuristic-fallback`. (The guard MCP *bridge* on `:8004` is wired in step 3 — this step is just
the model + `:8009` sidecar.)
- **Why now (before the bridges)** — wiring the guard bridge is pointless until the sidecar behind
it reports a real classifier; do the model first so step 3's `:8004` check is meaningful. Guard
is OFF by default and the board's `/security` toggle stays gated until the model's deps are ready.
- **Prereq** — **uv**, the **hf** CLI, and an **accepted Llama license** for the gated default.
No license / can't gate? Use the **heuristic-only** preset (zero-dependency, no download, no
token) — honest but degraded; you can upgrade to the model later.
- **Run** — invoke **`/guard-setup`** (or follow its steps: `hf auth login` → `hf download
meta-llama/Llama-Prompt-Guard-2-86M` → `uv sync --directory "$REPO_ROOT/guard" --extra model` →
set `COS_GUARD_MODEL`/`COS_GUARD_THRESHOLD` in `config/cos.env` →
`node "$REPO_ROOT/scripts/gen-launchd.mjs" --install guardsvc`).
- **CHECKPOINT** — the sidecar is up and reports the real model (not the heuristic):
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh"
curl -s "$GUARD_SIDECAR_URL/healthz"
# {"ok":true,"classifier":"model:meta-llama/Llama-Prompt-Guard-2-86M",...}
```
`classifier` must read `model:…` (or, deliberately, `heuristic-fallback` if you chose the
heuristic-only preset). A *silent* `heuristic-fallback` when you wanted the model = not done —
see guard-setup → Troubleshooting (force `COS_GUARD_CLASSIFIER=promptguard` to surface the cause).
### Step 3 — mcp-bridge-setup (wire the CORE servers + sidecars)
- **What it does** — wires the **four core** stdio MCP servers (**board, calendar, guard,
vault**) into both clients: **Cowork** spawns them directly as stdio `command` entries in
`claude_desktop_config.json`; **Claude Code** reaches them over HTTP via `$REPO_ROOT/.mcp.json`,
each a **supergateway + launchd** bridge on **8001/8003/8004/8005**. Also loads the two **uv
sidecars** — search `:8008` and guard `:8009` — and `mcp/ensure-bridges.sh` (chained into
`board`'s `dev`/`start`) so the app brings the bridges up. (The optional **openwhispr** voice
server on `:8002` is wired by **Step 3.4**'s add-on skill, not here.)
- **The vault bridge is special** — `:8005`, launchd label `com.chiefofstaff.mcp-vault`. Unlike the
other four it embeds the Agent SDK and makes outbound Anthropic calls, so it needs an
**`ANTHROPIC_API_KEY`**. The key is **NOT in the plist**: the plist's `ProgramArguments` runs
the launch wrapper **`mcp/vault-server/launch.sh`**, which sources the gitignored
**`config/secrets.env`** (copied from `config/secrets.env.example`) and exports the key before
exec'ing supergateway — so the secret lives in one machine-local file, never in the installed
plist or a committed file. `COS_VAULT_DIR` (= `$VAULT_DIR`, i.e. `$REPO_ROOT/vault/$VAULT_NAME`)
IS set in the installed plist (`EnvironmentVariables`) and the Cowork config; the plist is
generated from `mcp/vault-server/vault.service.json` by `scripts/gen-launchd.mjs` (it resolves
`$REPO_ROOT` / `$VAULT_DIR` from `config/load-config.sh` — see `mcp/CLAUDE.md`; the API key has no
placeholder, it stays in `config/secrets.env`). Restart it after editing the key (rotating the key
needs only this restart, no plist regen):
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh"; U=$(id -u)
launchctl bootout gui/$U/com.chiefofstaff.mcp-vault 2>/dev/null || true
launchctl bootstrap gui/$U "$LAUNCH_AGENTS_DIR/com.chiefofstaff.mcp-vault.plist"
launchctl kickstart -k gui/$U/com.chiefofstaff.mcp-vault
```
- **Prereq** — node + supergateway; the **vault from step 1** (the bridge's `COS_VAULT_DIR`
target); the **guard model from step 2** (so `:8004` reports the real classifier); an
**ANTHROPIC_API_KEY** in **`config/secrets.env`** (`cp config/secrets.env.example config/secrets.env`,
then edit in the `sk-ant-…` key) for the vault bridge.
- **Run** — invoke **`/mcp-bridge-setup`** (or follow its steps: `node "$REPO_ROOT/scripts/gen-launchd.mjs"
--install` renders + loads the core plists — board/calendar/guard/vault bridges on 8001/8003/8004/8005
plus the search + guardsvc sidecars; `node "$REPO_ROOT/scripts/gen-mcp-json.mjs"` regenerates the
Claude Code `.mcp.json`; `node "$REPO_ROOT/scripts/gen-cowork-config.mjs" board calendar guard vault`
merges the Cowork stdio entries; `mcp/ensure-bridges.sh`).
- **CHECKPOINT** — all four core bridges answer an MCP `initialize`, and the vault one is scoped:
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh"; U=$(id -u)
launchctl list | grep chiefofstaff # PIDs present, exit 0
for p in "$BOARD_BRIDGE_PORT" "$CALENDAR_BRIDGE_PORT" "$GUARD_BRIDGE_PORT" "$VAULT_BRIDGE_PORT"; do
curl -s -X POST "http://127.0.0.1:$p/mcp" \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"c","version":"0"}}}' \
| grep -o '"name":"[a-z]*"' | head -1
done
# expect: board / calendar / guard / vault
```
The vault server's ready line (`$REPO_ROOT/mcp/logs/vault.out.log`) must echo your
`COS_VAULT_DIR` (= `$VAULT_DIR`). `serverInfo.name=="vault"` confirms the vault bridge
(`$VAULT_BRIDGE_PORT`).
### Step 3.4 — openwhispr-mcp-setup (OPTIONAL: the voice-notes add-on)
- **What it does** — wires the **`openwhispr`** voice server (a Node stdio MCP fronted by a
supergateway + launchd bridge on `:8002`, plus a direct Cowork stdio entry) so **`/voice-operations`**
and **`/second-brain-ingest`** can read your OpenWhispr transcripts and route them onto the
board / vault. Entirely optional: skip it if you don't use the OpenWhispr desktop app.
- **Why HERE (after the core bridges)** — it reuses the same supergateway/launchd/`ensure-bridges.sh`
machinery Step 3 set up, and routing voice notes depends on the **board** + **vault** MCPs already
being live (Steps 1–3). Its only external state is the app's own store, so its position relative
to Step 4 doesn't matter.
- **Prereq** — node + supergateway (from Step 3) and the **OpenWhispr desktop app installed with at
least one recorded note** (so `$OPENWHISPR_DB` exists). No app yet? Skip this step, or wire it
against the bundled fixtures for a dry run (see the skill).
- **Run** — invoke **`/openwhispr-mcp-setup`** (confirm the store → install the `:8002` bridge plist
→ register both clients → wire `ensure-bridges.sh` → verify `list_transcripts` reports
`Source: sqlite`).
- **CHECKPOINT** — the openwhispr MCP answers and reads the real store:
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh"
curl -s -X POST "$OPENWHISPR_BRIDGE_URL/mcp" \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"c","version":"0"}}}' \
| grep -o '"name":"openwhispr"' && echo "openwhispr MCP OK"
```
### Step 3.5 — whatsapp-mcp-setup (OPTIONAL: the WhatsApp add-on)
- **What it does** — stands up the **external** `whatsapp-mcp` repo (the Go whatsmeow bridge as a
launchd SIDECAR on `:8010`, the Python stdio MCP as a supergateway BRIDGE on `:8006`), does the
one-time **QR pairing** against your phone, and registers `whatsapp` in `.mcp.json` + the Cowork
config — so the **`/whatsapp-triage`** skill can reconcile WhatsApp onto the board exactly like
`/mail-to-board` does Gmail. Entirely optional: skip it if you don't want WhatsApp on the board.
- **Why HERE (after the bridges)** — it reuses the same supergateway/launchd/`ensure-bridges.sh`
machinery Step 3 set up, and `/whatsapp-triage` depends on the **board** + **guard** MCPs already
being live (Steps 2–3). Its external `store/` is **not** covered by Step 4's backup (that protects
the Cos repo's own stores), so its position relative to Step 4 doesn't matter.
- **Prereq** — **go** (builds the bridge), **uv**, **supergateway**, the **whatsapp-mcp checkout**
at `$WHATSAPP_MCP_DIR`, and a **phone running WhatsApp** for the QR pairing.
- **Run** — invoke **`/whatsapp-mcp-setup`** (clone/build → QR pair → install both LaunchAgents →
register both clients → wire `ensure-bridges.sh` → verify `list_chats`).
- **CHECKPOINT** — the whatsapp MCP answers and returns chat data:
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh"
curl -s -X POST "$WHATSAPP_MCP_BRIDGE_URL/mcp" \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"c","version":"0"}}}' \
| grep -o '"name":"whatsapp"' && echo "whatsapp MCP OK"
```
### Step 3.6 — nutrition-mcp-setup (OPTIONAL: the Nutrition & Chef add-on)
- **What it does** — wires the built-in **`nutrition`** add-on MCP (a thin in-repo Node `fetch`
wrapper over the board's `/api/nutrition/*` routes, fronted by a supergateway + launchd bridge on
`:8007`, plus a direct Cowork stdio entry), then **ENABLES** the add-on
(`Settings.addons.nutrition.enabled`) so its food-log / pantry / meal-plan **writes** land and the
three `/nutrition/*` nav pages appear — so the **`/nutrition-chef`** operator skill can log meals,
track the pantry, and plan a week. Entirely optional and the SIMPLEST add-on (no sidecar, no
external repo, no secret); skip it if you don't want nutrition on the board. Enabling nutrition
also **hard auto-enables** the foundational `body` add-on (`dependsOn` in `board/lib/addons.ts`;
the cascade in `/api/addons/[id]`) — **Step 3.8** wires the `body` MCP that just came on.
- **Why HERE (after the core bridges)** — it reuses the same supergateway/launchd/`ensure-bridges.sh`
machinery Step 3 set up, and it is a pure wrapper over the **board** API (Steps 1–3 must be live).
Its data rides `cases.json`, so it is covered by Step 4's backup automatically — but its position
relative to Step 4 doesn't matter.
- **Prereq** — node + supergateway (from Step 3) and the **board** reachable on `CRM_BASE_URL`.
There is nothing external to install, clone, build, pair, or authenticate.
- **Run** — invoke **`/nutrition-mcp-setup`** (confirm `NUTRITION_BRIDGE_PORT=8007` → install the
`:8007` bridge plist → register both clients → ENABLE the add-on → verify a `log_food` write
round-trips).
- **CHECKPOINT** — the nutrition MCP answers and the add-on is enabled:
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh"
curl -s -X POST "$NUTRITION_BRIDGE_URL/mcp" \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"c","version":"0"}}}' \
| grep -o '"name":"nutrition"' && echo "nutrition MCP OK"
curl -s "$BOARD_URL/api/addons" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const a=JSON.parse(s).addons.find(x=>x.id==="nutrition");process.exit(a&&a.enabled===true?0:1)})' && echo "add-on enabled"
```
### Step 3.7 — fitness-mcp-setup (OPTIONAL: the Fitness add-on)
- **What it does** — wires the built-in **`fitness`** add-on MCP (a thin in-repo Node `fetch`
wrapper over the board's `/api/fitness/*` routes, fronted by a supergateway + launchd bridge on
`:8011` / `$FITNESS_BRIDGE_PORT`, plus a direct Cowork stdio entry), then **ENABLES** the add-on
(`Settings.addons.fitness.enabled`) so its writes land and the `/fitness` + `/fitness/*` nav pages
appear — so the 7-skill fitness operator family (**`/fitness-coach`**, **`/fitness-training-plan`**,
…) can drive it. Enabling fitness also **hard auto-enables** the foundational `body` add-on
(`dependsOn` in `board/lib/addons.ts`) — **Step 3.8** wires the `body` MCP that just came on.
Entirely optional; skip it if you don't want fitness on the board.
- **Why HERE (after the core bridges)** — it reuses the same supergateway/launchd/`ensure-bridges.sh`
machinery Step 3 set up, and it is a pure wrapper over the **board** API (Steps 1–3 must be live).
Its data rides `cases.json`, so it is covered by Step 4's backup automatically.
- **Prereq** — node + supergateway (from Step 3) and the **board** reachable on `CRM_BASE_URL`. The
loader seeds `FITNESS_BRIDGE_PORT=8011` when `cos.env` lacks it — no `cos.env` edit needed.
- **Run** — invoke **`/fitness-mcp-setup`** (confirm the port → verify the stdio server → the
`.mcp.json` entry → install the `:8011` bridge → register Cowork → ENABLE the add-on → verify a
round-trip — its §1–§7).
- **CHECKPOINT** — mirror 3.6's: the fitness MCP answers and the add-on is enabled:
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh"
curl -s -X POST "$FITNESS_BRIDGE_URL/mcp" \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"c","version":"0"}}}' \
| grep -o '"name":"fitness"' && echo "fitness MCP OK"
curl -s "$BOARD_URL/api/addons" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const a=JSON.parse(s).addons.find(x=>x.id==="fitness");process.exit(a&&a.enabled===true?0:1)})' && echo "add-on enabled"
```
### Step 3.8 — body-mcp-setup (CONDITIONAL: the foundational Body add-on — REQUIRED after 3.6 or 3.7)
- **What it does** — wires the built-in **`body`** add-on MCP (a thin in-repo Node `fetch` wrapper
over the board's `/api/body/*` routes, fronted by a supergateway + launchd bridge on `:8012` /
`$BODY_BRIDGE_PORT`, plus a direct Cowork stdio entry). `body` is the **single owner** of body
identity, the weight + body-composition series, and the free-text objective that Nutrition and
Fitness both read.
- **Why this step is REQUIRED if you ran 3.6 or 3.7** — enabling nutrition or fitness already
**auto-enabled `body`** (the hard-dependency cascade in `/api/addons/[id]`), so the `/body` nav and
`/api/body` prefix are live NOW — but until this step runs, that surface has no agent access, and
no other skill wires it. This is the exact invisible step cos-ops#35 closes. **Skip only if you
skipped BOTH 3.6 and 3.7.** Running body **standalone** (weight tracking alone, with neither
nutrition nor fitness) is also legitimate — see the skill's §6 to enable it directly.
- **Run** — invoke **`/body-mcp-setup`** (same §1–§7 flow as 3.6/3.7; its §6 *confirms* the
auto-enable rather than flipping it).
- **CHECKPOINT** — the body MCP answers and the add-on is enabled:
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh"
curl -s -X POST "$BODY_BRIDGE_URL/mcp" \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"c","version":"0"}}}' \
| grep -o '"name":"body"' && echo "body MCP OK"
curl -s "$BOARD_URL/api/addons" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const a=JSON.parse(s).addons.find(x=>x.id==="body");process.exit(a&&a.enabled===true?0:1)})' && echo "add-on enabled"
```
### Step 4 — backup-recovery (LAST: protect the now-populated stores)
- **What it does** — stands up **daily AES-256-GCM-encrypted, off-site** snapshots of the live
stores (board `board/data/`, guard `guard/data/`, `config/`, and the **vault**) to a private
GitHub repo, with the recovery key in the macOS Keychain (`cos-backup-key`) + an offline copy.
- **Why LAST** — backup snapshots the **populated** stores; running it before the vault exists and
the bridges are wired would back up an empty/partial system. The backup `SCOPE` already covers the
active vault — it auto-derives from the `VAULT_NAME` you recorded in step 1.
- **Prereq** — a **private GitHub backup repo** (e.g. `gh repo create cos-backups --private`) and a
**recovery passphrase** stored in the macOS Keychain as `cos-backup-key` (plus a password-manager
copy — it is unrecoverable). The backup LaunchAgent (`com.chiefofstaff.backup`, daily 03:30)
reads `backup/config.mjs` at run time, so SCOPE edits need no restart.
- **Run** — invoke **`/backup-recovery`** (§1 Setup: generate + store the key, create + clone the
repo, first backup + verify, install the daily LaunchAgent).
- **CHECKPOINT** — the key exists and a backup verifies end-to-end:
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh"; cd "$REPO_ROOT"
security find-generic-password -s cos-backup-key -w >/dev/null 2>&1 && echo "key OK"
node backup/backup.mjs && node backup/restore.mjs # backup, then DRY-RUN verify (no writes)
# → auth tag OK ✓ / sha256 OK ✓ / JSON-verified ✓ ; and "vault/$VAULT_NAME" appears in the manifest
```
---
## End-to-end verification (the whole system, after all four steps)
Run from the repo root with the board dev app up (`cd board && npm run dev`, port `$BOARD_PORT`).
Start each shell with `source "$(git rev-parse --show-toplevel)/config/load-config.sh"`:
1. **All four core bridges answer** (the step-3 loop) → `board / calendar / guard / vault` (plus
`openwhispr` on `:8002`, `whatsapp` on `:8006`, `nutrition` on `:8007`, `fitness` on `:8011`,
and/or `body` on `:8012` if you ran the Step 3.4–3.8 add-ons). `launchctl list | grep
chiefofstaff` shows each with exit 0.
2. **Guard sidecar healthy** — `curl -s "$GUARD_SIDECAR_URL/healthz"` → `{"ok":true,
"classifier":"model:…"}` (or the deliberate `heuristic-fallback`). The **search** sidecar is
best-effort: `curl -s "$SEARCH_SIDECAR_URL/healthz"` → `{"ok":true}` (a cold/absent one just
degrades the board to keyword search — not a failure).
3. **A test vault ingest + query round-trips** — drop a throwaway note into the vault and read it
back through the vault MCP (`$VAULT_BRIDGE_URL`); the inner Agent SDK session is scoped to your
`COS_VAULT_DIR` (= `$VAULT_DIR`), so it must find what you just wrote and return it.
(Equivalently, use `/second-brain-ingest` then `/second-brain-query`.) Remove the throwaway note
after.
4. **A backup dry-run verifies** — `node backup/backup.mjs && node backup/restore.mjs` →
`auth tag OK ✓ / sha256 OK ✓ / JSON-verified ✓`, with `vault/$VAULT_NAME` present in the
manifest.
5. **The Cowork config is actually wired** (don't just assume it — it's the one client whose file is
off-repo at `$COWORK_CONFIG`): the file exists, parses, and lists the core stdio servers.
```sh
node -e 'const fs=require("fs"),p=process.env.COWORK_CONFIG;let c;try{c=JSON.parse(fs.readFileSync(p,"utf8"))}catch(e){console.log("FAIL: Cowork config missing/invalid at "+p+" — set COWORK_CONFIG in cos.env + re-run /mcp-bridge-setup §5");process.exit(1)}const m=c.mcpServers||{},miss=["board","calendar","guard","vault"].filter(n=>!m[n]);console.log(miss.length?"FAIL: missing from Cowork config: "+miss.join(", ")+" — re-run /mcp-bridge-setup §5":"OK: Cowork config lists "+Object.keys(m).join(", "))'
```
(Cowork only *reads* it at launch — the next section is the one-time UI activation; this just
proves the file is on disk and correct first.)
If all five pass, the Cos system is fully stood up: vault populated, guard classifying,
all bridges live for Cowork + Claude Code, and the live data under encrypted off-site backup.
Tell the user so, then hand off with the **first-open** and **Day-to-day** notes below.
## First open in Claude Cowork — confirm the connectors + allow their tools
The config is wired, but **Cowork reads `claude_desktop_config.json` only at launch** and gates tool
calls behind a permission prompt. Walk the user through the one-time activation:
1. **Quit + reopen Cowork (⌘Q)** so it re-reads the config.
2. **Settings → Connectors** — confirm the local MCP servers (**board**, **calendar**, **guard**,
**vault**, plus **openwhispr**/**whatsapp**/**nutrition**/**fitness**/**body** if added) are listed
and enabled. They run as local stdio `command` servers (not custom HTTP connectors), so they
appear automatically once the config is read — if they don't, it didn't parse: re-check §5 of
**/mcp-bridge-setup**.
3. **Allow their tools** — the first time an agent calls a server's tool, Cowork asks for permission;
choose **"Always allow"** per server so routine agent runs aren't interrupted by a prompt every
call (or approve per-tool if you prefer — "Always allow" is the smooth default for your own local
servers).
(Claude Code uses the HTTP bridges via `.mcp.json` and skips this — the connector-approval step is
Cowork-only.)
### Step 5 — operator automation (install the skill bundles, then schedule the sweeps)
- **What it does** — installs every operator skill bundle in `$REPO_ROOT/board/.claude/skill-bundles/`
into Cowork, then creates a Cowork **Scheduled Task** for each **scheduled** row of the generated
catalog in `board/.claude/skills/README.md` (built from `board/.claude/skills/automation.json`) —
pasting that row's trigger line at its suggested cadence.
- **Why every bundle, not just the scheduled ones** — a **called** skill (`vault-operations`) is
invoked by other skills mid-run, and in Cowork a delegation to a skill that is not installed is a
**silent no-op, not an error** — the class of bug that left the vault unfed for weeks with nothing
in any log to say so (ADR 0015). Installing every bundle costs one extra upload per skill and closes
that failure mode structurally.
- **Why LAST** — it needs Cowork **open** with its connectors **allowed** (the previous section), and
it is the step that turns "the board can do this" into "the board actually does this on its own" —
the automation Philip asked for is exactly this catalog, instantiated.
- **Run**:
1. **Cowork Desktop → Settings → Capabilities → Skills → Upload skill**, once per `.zip` in
`$REPO_ROOT/board/.claude/skill-bundles/`.
2. **Cowork Desktop → Scheduled Tasks → new task**, one per row of the **scheduled** table in
`board/.claude/skills/README.md` — paste that row's trigger, and pick a cadence (the table's
column is a **suggestion**; the schedule is yours to set and adjust — see `docs/architecture/
triage-skills.md`).
- **CHECKPOINT** — manual, and honest about it: nothing on this machine can read Cowork's installed
state back (ADR 0020's standing consequence), so this is a look, not a `curl`. In **Cowork Desktop
→ Settings → Capabilities → Skills**, every bundle you uploaded is listed; in **Scheduled Tasks**,
every task you chose to create is listed, at the cadence you picked.
## Day-to-day: running Cos in later sessions
After setup, most of it runs itself — make sure the user knows how to live with it and how to check health:
- **The bridges + sidecars are launchd-managed.** The core four (`board`, `calendar`, `guard`,
`vault`), the `search`/`guardsvc` uv sidecars, and any optional add-ons you wired
(`openwhispr`/`whatsapp`/`nutrition`/`fitness`/`body`) start at login and **crash-restart** on
their own (`KeepAlive`). A normal next session needs **no action** — Cowork and Claude Code reach
them through the bridges whether or not the board dev app is running.
- **Starting the board self-heals them.** `cd board && npm run dev` (or `npm run start`) runs
`mcp/ensure-bridges.sh` *first*, which bootstraps + kickstarts every service and prints one line each
(`[mcp] vault bridge up on :$VAULT_BRIDGE_PORT` … or `WARN: <name> bridge DOWN on :<port> — see
mcp/logs/<name>.err.log`). Reading that startup block IS the fastest health check.
- **One-shot health check anytime:** run `mcp/ensure-bridges.sh` directly — it is re-runnable and never
stops anything, so it's the canonical "is everything up?" probe. Backstops: `launchctl list | grep
chiefofstaff` (each agent + last exit code; `0` = clean) and `curl -s "$GUARD_SIDECAR_URL/healthz"` /
`curl -s "$SEARCH_SIDECAR_URL/healthz"` for the sidecars.
- **In-app health surfaces:** **`/security`** (guard model, deps, master toggle), **`/backups`** (last
snapshot + recovery-key readiness), **`/vault`** (vault wiring + Obsidian deep-link).
- **When a client can't see a server:** re-run `mcp/ensure-bridges.sh`; if it's still missing, re-run
**/mcp-bridge-setup**. Per-service logs for any WARN/DOWN live in `mcp/logs/<name>.{err,out}.log`.
- **Pulling an update later is a different skill: `/cos-upgrade`.** A `git pull` migrates the store
on the next board start and nothing else — the production board, the launchd bridges, the Cowork
bundles and scheduled tasks keep running the old version silently. `/cos-upgrade` backs up, pulls,
runs `node scripts/upgrade-check.mjs` (the diff → the ordered checklist), applies the automatable
steps, and hands you the manual residue. Never re-run `cos-setup` to upgrade.
- **The two standing gestures:** Guard ships **OFF** — flip it ON in `/security` once its model deps are
ready (or deliberately leave it OFF; see **/guard-setup**). Backups then run **nightly at 03:30** on
their own once **/backup-recovery** is set up.
## If something fails
Each component skill owns its own troubleshooting — jump straight there:
**/setup-vault** (template/gitignore/SCOPE), **/guard-setup** (GatedRepoError, silent heuristic
fallback, cold sidecar), **/mcp-bridge-setup** (the node/simdjson + pm2 gotchas, a bridge DOWN,
Cowork can't see a server), **/backup-recovery** (bad magic / auth-tag throw / sha256 mismatch on
restore). Re-run only the failing step, re-check its CHECKPOINT, then resume the sequence.
Scanned 9/2/2026
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!