Bootstrap a NEW private vault instance from the committed example-vault template — copy the template to vault/[name], point the vault MCP bridge (:8005) at it via COS_VAULT_DIR, register it with Obsidian + capture its unique vault ID into settings.json (so the board's obsidian:// deep-links open the in-repo vault unambiguously), register it with backup, and confirm it is gitignored. Use when the user says "set up a new vault", "create my vault", "spin up a new knowledge base", wants a fresh p...
Scanned 9/2/2026
Install to Claude Code
npx -y skills add philipyaz/cos --skill setup-vault --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Setup Vault?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/philipyaz-setup-vault)More formats (shields.io, HTML) on the badges page.
---
name: setup-vault
description: Bootstrap a NEW private vault instance from the committed example-vault template — copy the template to vault/[name], point the vault MCP bridge (:8005) at it via COS_VAULT_DIR, register it with Obsidian + capture its unique vault ID into settings.json (so the board's obsidian:// deep-links open the in-repo vault unambiguously), register it with backup, and confirm it is gitignored. Use when the user says "set up a new vault", "create my vault", "spin up a new knowledge base", wants a fresh private knowledge base, or when Obsidian vault deep-links from the board open the wrong vault — and as the vault step of the cos-setup orchestrator (which sequences the MCP bridge after this).
allowed-tools: Bash, Read, Write, Edit
---
# Set up a new private vault
## What a vault is
A **vault** is the chief-of-staff's private knowledge base — real PII lives here (work/life notes,
entities, concepts, sources). It is **knowledge-only** (no to-dos, reminders, or priorities — those
live on the board). The repo ships ONE committed, synthetic template,
`vault/example-vault/`; a real instance is a **copy** at `vault/<name>/` that is **never committed**
(durability comes from `backup/`, encrypted + off-site — NOT git). The vault MCP server
(`mcp/vault-server/server.mjs`, bridged to `:8005`) reads/writes exactly ONE vault, chosen by the
`COS_VAULT_DIR` env var in its launchd plist.
The loader (`source "$(git rev-parse --show-toplevel)/config/load-config.sh"`, run at the top of
**every** shell block below) exports `$REPO_ROOT`, `$BREW_PREFIX`, `$LAUNCH_AGENTS_DIR`, `$COWORK_CONFIG`,
`$VAULT_NAME`, `$VAULT_DIR`, the bridge ports/URLs, etc. — use those instead of hardcoding machine
paths. **Each fenced block runs in a fresh shell — shell state does NOT persist between blocks — so the
loader line must begin every block, or its vars are empty.** `$U=$(id -u)` stays inline where
`launchctl` needs it; `<name>` is the slug from STEP 1.
**`$name` does not carry across blocks either.** It is set once in STEP 1, but every later block runs in
its own shell where `$name` is empty. So in each block below you MUST substitute the **literal chosen
slug** in place of `<name>`/`$name` (e.g. write `alice-knowledge`, not `$name`) — otherwise empty-var
paths like `$REPO_ROOT/vault/$name` collapse to the vault PARENT dir and corrupt the installed plist.
Blocks that write the plist re-validate the slug as a guard.
This skill creates the instance and wires it. It does **not** install the bridge itself — that is
**mcp-bridge-setup**. From `cos-setup`, run mcp-bridge-setup AFTER this (it sequences them); standalone,
the installed plist must already exist (see STEP 3).
---
## STEP 1 — Choose a name
Ask the user for a vault name: a clean **lowercase-dashed slug** (e.g. `<your-vault>`,
`alice-knowledge`). Validate before doing anything:
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh"
# The loader exports $REPO_ROOT, $BREW_PREFIX, $LAUNCH_AGENTS_DIR, $COWORK_CONFIG, $VAULT_NAME,
# $VAULT_DIR, the bridge ports/URLs, etc. — use those instead of hardcoding machine paths.
name="<name>" # the user's answer
# slug-only: lowercase letters, digits, dashes; not leading/trailing/double dash
case "$name" in
""|*[!a-z0-9-]*|-*|*-|*--*) echo "REJECT: not a clean lowercase-dashed slug"; exit 1;; esac
[ "$name" = "example-vault" ] && { echo "REJECT: example-vault is the template, not an instance"; exit 1; }
[ -e "$REPO_ROOT/vault/$name" ] && { echo "REJECT: vault/$name already exists"; exit 1; }
echo "OK: $name"
```
Reject and re-ask if: empty, contains anything but `[a-z0-9-]`, equals `example-vault`, or
`vault/<name>` already exists.
## STEP 2 — Copy the template
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh" # gives $REPO_ROOT (fresh shell)
name="<name>" # the literal slug from STEP 1 — vars don't carry across blocks
cp -R "$REPO_ROOT/vault/example-vault" "$REPO_ROOT/vault/$name"
```
Confirm the structure copied (key dirs/files present):
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh" # gives $REPO_ROOT (fresh shell)
name="<name>" # the literal slug from STEP 1
ls -1 "$REPO_ROOT/vault/$name"
# expect: CLAUDE.md, aliases.md, life/, output/, raw/, shared/, work/ (+ hidden .claude/)
test -f "$REPO_ROOT/vault/$name/work/wiki/index.md" \
&& test -f "$REPO_ROOT/vault/$name/life/wiki/index.md" \
|| echo "WARN: template incomplete"
```
(The vault is a **knowledge-only, domain-split LLM-wiki**: each domain has its own wiki — `work/wiki/`
and `life/wiki/`, each an `index.md` + `concepts/ entities/ sources/ log.md` — plus
`shared/wiki/entities/` for the truly-dual entities, alongside `raw/` (+ `raw/assets/`), `output/`, and
`aliases.md`. There is no top-level `wiki/`, and no reminders/priorities — actionable state lives on the
board. If a layout key you expect is missing, the template moved — `cp -R` copies whatever
`example-vault` currently holds.)
## STEP 3 — Point the MCP at it (COS_VAULT_DIR)
First, record the chosen slug as the active vault in `config/cos.env` — this is the SKILL-side
source of truth (the loader resolves `$VAULT_DIR = $REPO_ROOT/vault/$VAULT_NAME` from it for every
skill thereafter):
```bash
# Record the active vault in config/cos.env so skills resolve $VAULT_DIR to it.
source "$(git rev-parse --show-toplevel)/config/load-config.sh" # gives $REPO_ROOT (fresh shell)
name="<name>" # the literal lowercase-dashed slug chosen in STEP 1 — vars don't carry across blocks
case "$name" in (*[!a-z0-9-]*|"") echo "vault name must be a lowercase-dashed slug"; exit 1;; esac
tmp="$(mktemp)"
grep -v '^VAULT_NAME=' "$REPO_ROOT/config/cos.env" > "$tmp" 2>/dev/null || true
printf 'VAULT_NAME="%s"\n' "$name" >> "$tmp"
mv "$tmp" "$REPO_ROOT/config/cos.env"
grep '^VAULT_NAME=' "$REPO_ROOT/config/cos.env" # confirm exactly one line, the new name
```
`COS_VAULT_DIR` flows from `VAULT_NAME` into TWO places: the **installed launchd plist** (it is
**`${VAULT_DIR}`** in the vault descriptor, resolved at generation time) and the **Cowork config**.
**Installed plist** — `$LAUNCH_AGENTS_DIR/com.chiefofstaff.mcp-vault.plist`. You do **not** hand-edit
its `COS_VAULT_DIR` `<string>` anymore: the plist is generated from
`mcp/vault-server/vault.service.json` by `scripts/gen-launchd.mjs`, which resolves `COS_VAULT_DIR =
${VAULT_DIR}` from the `VAULT_NAME` you just recorded (see `mcp/CLAUDE.md`). Regenerate + reload it in
one step — the generator renders the plist AND does `launchctl bootout`→`bootstrap`→`kickstart` on
macOS. (If this is `cos-setup`, the orchestrator runs the MCP step AFTER this with `gen-launchd
--install` for all core services — note it and skip this command for now; the regenerated plist will
pick up your `VAULT_NAME` then.)
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh" # gives $REPO_ROOT, $VAULT_DIR (fresh shell)
# VAULT_NAME (recorded above) drives $VAULT_DIR → the generated COS_VAULT_DIR. No slug substitution here.
node "$REPO_ROOT/scripts/gen-launchd.mjs" --install vault
grep -A1 COS_VAULT_DIR "$LAUNCH_AGENTS_DIR/com.chiefofstaff.mcp-vault.plist" # confirm it reads $VAULT_DIR
```
**Async ingest runner** — the vault also has a detached jobs-runner sidecar
(`com.chiefofstaff.mcp-vaultjobs`) that executes async `ingest` jobs (see
[docs/reference/vault-async.md](../../../docs/reference/vault-async.md)). It is generated from its
descriptor `mcp/vault-server/vaultjobs.service.json` the same way — `COS_VAULT_DIR = ${VAULT_DIR}`,
needs `ANTHROPIC_API_KEY` (its launch wrapper `jobs-runner-launch.sh` sources `config/secrets.env`),
and has no port. It is `optional`, so name it explicitly:
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh" # gives $REPO_ROOT
node "$REPO_ROOT/scripts/gen-launchd.mjs" --install vaultjobs
launchctl list | grep com.chiefofstaff.mcp-vaultjobs # loaded?
tail -n 3 "$REPO_ROOT/mcp/logs/vaultjobs.err.log" # expect "[vault-jobs] runner up …" with NO fatal line after it
```
**Load the `vault-operations` skill in Cowork** — the
[`vault-operations`](https://github.com/philipyaz/cos/tree/main/board/.claude/skills/vault-operations) skill
teaches the model the async **submit-then-poll** ingest lifecycle (and never to re-submit an in-flight
job). Claude **Code** auto-loads it from the repo's `board/.claude/skills/` — nothing to do there.
Claude **Cowork Desktop** does NOT read the repo filesystem; custom skills are added through its
**UI** by uploading a ZIP — upload the already-committed
`board/.claude/skill-bundles/vault-operations.zip` (same two clicks as every other skill; no zip step
to run yourself):
In Claude Cowork Desktop: **Customize → `+` next to Skills → Create skill → select
`board/.claude/skill-bundles/vault-operations.zip`**. It is available immediately (no restart needed)
across all Cowork sessions; a future edit to the skill is picked up by re-uploading the bundle
`scripts/pack-skills.mjs` rewrites.
> Belt-and-braces: even if the skill isn't loaded, the `ingest` / `ingest_status` tool **descriptions**
> already instruct the model to submit-then-poll and not to re-submit an in-flight job — the skill
> reinforces that guidance, it isn't strictly required for correctness.
**Cowork config (if configured)** — `$COWORK_CONFIG`, the `"vault"` server entry's
`env.COS_VAULT_DIR`. Only touch it if a `"vault"` entry exists:
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh" # gives $REPO_ROOT, $COWORK_CONFIG (fresh shell)
name="<name>" # the literal slug from STEP 1
case "$name" in (*[!a-z0-9-]*|"") echo "bad slug"; exit 1;; esac
if [ -f "$COWORK_CONFIG" ] && grep -q '"vault"' "$COWORK_CONFIG"; then
/usr/bin/perl -0pi -e \
"s#(\"COS_VAULT_DIR\"\s*:\s*\")[^\"]*(\")#\${1}$REPO_ROOT/vault/$name\${2}#" "$COWORK_CONFIG"
grep COS_VAULT_DIR "$COWORK_CONFIG"
else echo "No Cowork vault entry — skipping (mcp-bridge-setup wires Cowork)."; fi
```
**Verify the bridge** (the `gen-launchd --install vault` above already reloaded it; just confirm):
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh" # gives $REPO_ROOT, $VAULT_BRIDGE_PORT (fresh shell)
sleep 2 # supergateway + node cold start
# initialize → serverInfo.name must be "vault"
curl -s -X POST "http://127.0.0.1:$VAULT_BRIDGE_PORT/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":"setup-vault","version":"1"}}}' \
| grep -o '"name":"vault"' && echo "BRIDGE OK :$VAULT_BRIDGE_PORT name=vault"
# the server ready line must echo the NEW COS_VAULT_DIR
grep COS_VAULT_DIR "$REPO_ROOT/mcp/logs/vault.out.log" | tail -1
```
`serverInfo.name=="vault"` AND the ready line echoing `$VAULT_DIR` (= `$REPO_ROOT/vault/<name>`) = bridge
is up and pointed at the new vault. (The vault server also needs a real `ANTHROPIC_API_KEY` for its
outbound LLM calls — this lives in the gitignored `config/secrets.env`, loaded by the launch wrapper,
NOT in the plist; mcp-bridge-setup sets it up. A cold start may connection-refuse for a few seconds —
re-probe.)
## STEP 3.5 — Register with Obsidian & capture the vault ID (deep-links)
The board's case drawer renders an `obsidian://open?vault=…&file=…` deep-link for each vault
wikilink. Obsidian resolves `vault=` against the vaults it has **registered** (in
`~/Library/Application Support/obsidian/obsidian.json`) — by the folder **basename** OR by a
unique **16-char vault ID**; it does NOT open an arbitrary on-disk folder. Two problems this
step fixes: **(a)** a fresh `vault/<name>` is unknown to Obsidian until you open it once, and
**(b)** if another registered vault shares the basename (e.g. an older copy elsewhere on disk),
a name-based link is **ambiguous** and opens the wrong one. So we register the folder and record
its **unique ID** into `config/settings.json` — the board reads it via `board/lib/vault-config.ts`
and builds the deep-link from the ID, which is unambiguous. Note the board also **self-heals**:
when `settings.json` has no id, `vault-config.ts` reads it *through* from `obsidian.json` by
realpath — so **opening the vault in Obsidian is already enough** for deep-links and the /vault
"Registered with Obsidian" check; the capture below just **persists** it (and lets it override).
> Registration is a **manual GUI action** a skill cannot perform: Obsidian has no headless
> "register a folder" command, and `obsidian://open?path=…` only searches ALREADY-registered
> vaults, so it can't bootstrap an unknown folder. We instruct, then read back the result.
**1. Open the folder as a vault in Obsidian** — print the path and ask the user to do it:
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh" # gives $REPO_ROOT (fresh shell)
name="<name>" # the literal slug from STEP 1
echo "In Obsidian: File → Open Vault → Open folder as vault → choose this folder:"
echo " $REPO_ROOT/vault/$name"
```
If Obsidian is not installed, skip to the **Fallback** at the end of this step.
**2. Capture the vault ID + name into `config/settings.json`.** This **polls** Obsidian's registry
(up to ~60s — so you can start it and THEN do Open-folder-as-vault), finds the entry whose path
matches THIS vault (realpath-compared, trailing-slash tolerant), and writes `obsidianVaultId` +
`obsidianVaultName` without touching other keys (creates `settings.json` from the example if absent).
The poll is the fix for the classic failure mode — running the capture ONCE, before the vault was
opened, silently recorded a **blank** id and the skill moved on:
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh" # gives $REPO_ROOT, $NODE_BIN (fresh shell)
name="<name>" # the literal slug from STEP 1
case "$name" in (*[!a-z0-9-]*|"") echo "bad slug"; exit 1;; esac
[ -f "$REPO_ROOT/config/settings.json" ] || cp "$REPO_ROOT/config/settings.example.json" "$REPO_ROOT/config/settings.json"
echo "Waiting for Obsidian to register $REPO_ROOT/vault/$name (do File → Open Vault → Open folder as vault now)…"
for i in $(seq 1 30); do
VAULT_PATH="$REPO_ROOT/vault/$name" \
SETTINGS="$REPO_ROOT/config/settings.json" \
OBSIDIAN_JSON="$HOME/Library/Application Support/obsidian/obsidian.json" \
"$NODE_BIN" -e '
const fs = require("fs"), path = require("path");
const real = (p) => { try { return fs.realpathSync(p); } catch { return path.resolve(p); } };
const want = real(process.env.VAULT_PATH);
let id = null, vname = path.basename(want);
try {
const reg = JSON.parse(fs.readFileSync(process.env.OBSIDIAN_JSON, "utf8")).vaults || {};
for (const [k, v] of Object.entries(reg)) {
if (v && typeof v.path === "string" && real(v.path) === want) { id = k; vname = path.basename(v.path); break; }
}
} catch {}
let s = {};
try { s = JSON.parse(fs.readFileSync(process.env.SETTINGS, "utf8")); } catch {}
s.obsidianVaultName = vname;
if (id) s.obsidianVaultId = id; else if (!("obsidianVaultId" in s)) s.obsidianVaultId = "";
fs.writeFileSync(process.env.SETTINGS, JSON.stringify(s, null, 2) + "\n");
if (id) { console.log(`OK: obsidianVaultId=${id} obsidianVaultName=${vname} → settings.json`); process.exit(0); }
process.exit(1); // not registered yet → keep polling
' && break
sleep 2
done
grep -E 'obsidianVault(Id|Name)' "$REPO_ROOT/config/settings.json" # confirm both keys
```
A 16-char hex `obsidianVaultId` = deep-links are unambiguous and persisted per-machine. If the loop
times out with a **blank** id, Obsidian still isn't aware of the folder — but this is no longer a
dead end: the board **self-detects** the id by reading it through from `obsidian.json` (realpath
match) the moment you Open-folder-as-vault, so the ↗ link and the /vault check start working on the
next **Refresh** even before it's persisted. Re-run this block any time to write the id into
`settings.json` (the canonical override that also survives an Obsidian registry reset).
> The vault ID is **per-registration**: if the user removes and re-adds the vault in Obsidian, a
> new ID is minted — re-run step 2 to re-capture it. `settings.json` is gitignored + per-machine
> (like `principalEmail`), so the ID never travels between machines.
**Fallback (no Obsidian / not yet opened):** the board still works — the in-app vault **preview**
reads the folder directly via `/api/vault` regardless. Only the ↗ "open in Obsidian" link needs the
ID. Install Obsidian → Open-folder-as-vault → re-run step 2 to enable deep-links.
## STEP 4 — Confirm backup scope (auto-derived — no hand-edit)
`backup/config.mjs` no longer hardcodes the vault. It derives `VAULT_SCOPE_PATH = vault/<VAULT_NAME>`
from `config/cos.env`'s `VAULT_NAME` — the slug you recorded in **STEP 3** — and folds it into `SCOPE`
automatically (precedence: `VAULT_NAME` env > `cos.env` > the historical default). So the ACTIVE vault
is always captured; a renamed/relocated vault can't silently fall out of scope, and there is **nothing
to hand-edit here**. Just confirm it resolves to `vault/<name>`:
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh" # gives $REPO_ROOT, $NODE_BIN (fresh shell)
name="<name>" # the literal slug from STEP 1
"$NODE_BIN" --input-type=module -e \
"import('$REPO_ROOT/backup/config.mjs').then(m => console.log('VAULT_SCOPE_PATH =', m.VAULT_SCOPE_PATH, '| in SCOPE:', m.SCOPE.includes(m.VAULT_SCOPE_PATH)))"
# expect: VAULT_SCOPE_PATH = vault/<name> | in SCOPE: true
```
If it prints the OLD vault (or the historical `my-personal-thoughts-vault` default), `VAULT_NAME` didn't
land in `config/cos.env` — re-run the STEP 3 `cos.env` block. No LaunchAgent restart — the backup agent
(`com.chiefofstaff.backup`, daily 03:30) reads `config.mjs` at run time. Backups only actually run once
**backup-recovery** setup is done (recovery key in Keychain `cos-backup-key`); note that to the user if unsure.
## STEP 5 — Confirm it is gitignored
The vault holds real private data — it is **NEVER committed**; durability is `backup/` (encrypted,
off-site), not git. The `.gitignore` pattern `/vault/*` + `!/vault/example-vault/` auto-ignores every
`vault/<name>` while keeping the committed template tracked. Verify:
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh" # gives $REPO_ROOT (fresh shell)
name="<name>" # the literal slug from STEP 1
cd "$REPO_ROOT"
git check-ignore "vault/$name" && echo "GITIGNORED ✓" || echo "NOT IGNORED — FIX .gitignore"
```
`git check-ignore` must **print the path** (= it is ignored). If it prints nothing, the broad pattern
is not in place — add these two lines to `.gitignore` (and remove any single stale
`vault/<old-vault>/` line) so EVERY instance is covered, then re-run the check:
```
/vault/*
!/vault/example-vault/
```
(Editing `.gitignore` is outside this skill's normal scope — only do it if the user asks; otherwise
flag the gap loudly so private data can't be committed by accident.)
## STEP 6 — Verify & report
Run the four-line summary and report it to the user:
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh" # gives $REPO_ROOT, $VAULT_BRIDGE_PORT (fresh shell)
name="<name>" # the literal slug from STEP 1
cd "$REPO_ROOT"
echo "structure: $(ls "$REPO_ROOT/vault/$name" 2>/dev/null | tr '\n' ' ')"
echo "bridge: $(curl -s -X POST "http://127.0.0.1:$VAULT_BRIDGE_PORT/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":"v","version":"1"}}}' | grep -o '"name":"vault"' || echo down) → $(grep COS_VAULT_DIR "$REPO_ROOT/mcp/logs/vault.out.log" | tail -1)"
echo "obsidian: $(grep -o '"obsidianVaultId": *"[^"]*"' "$REPO_ROOT/config/settings.json" 2>/dev/null || echo 'not set — ↗ deep-links disabled until Open-folder-as-vault (STEP 3.5)')"
echo "backup: scopes vault/$name (cos.env VAULT_NAME=$(grep -m1 '^VAULT_NAME=' "$REPO_ROOT/config/cos.env" | cut -d= -f2- | tr -d '\"'))"
echo "gitignore: $(git check-ignore "vault/$name" || echo 'NOT IGNORED')"
```
Tell the user: **vault `<name>` is ready.** To populate it, drop files in its `raw/` folder or call
the **vault MCP `ingest` tool** (via Cowork or Claude Code, driven per **`/vault-operations`**),
which classifies and files items into the per-domain (`work/`, `life/`, `shared/`) wikis.
---
## Troubleshooting
- **Bridge `down` / connection refused** — supergateway + node cold start takes a few seconds after
`kickstart`; re-probe. Persistent failure: `tail -f "$REPO_ROOT/mcp/logs/vault.err.log"`.
- **Tools return an auth error / `session failed`** — a missing/empty/invalid `ANTHROPIC_API_KEY` in
`config/secrets.env` (the launch wrapper sources it). The bridge still boots (fail-soft); fix the key
and `launchctl kickstart -k gui/$(id -u)/com.chiefofstaff.mcp-vault`. Note `COS_VAULT_MODEL` must be a
valid public-API model id (default `claude-sonnet-4-6`) — a stale id 404s with `not_found_error`.
- **Ready line still shows the OLD vault** — `VAULT_NAME` didn't land in `config/cos.env` (re-run the
STEP 3 `cos.env` block) or you didn't regenerate the plist. Re-check
`grep -A1 COS_VAULT_DIR "$LAUNCH_AGENTS_DIR/com.chiefofstaff.mcp-vault.plist"`, then
`node "$REPO_ROOT/scripts/gen-launchd.mjs" --install vault` again (it bootout/bootstrap/kickstarts for you).
- **`git check-ignore` prints nothing** — the broad `/vault/*` + `!/vault/example-vault/` pattern isn't
in `.gitignore` (a single `vault/<old-name>/` line only covers that one name). Add the two lines
above. NEVER `git add` a real vault.
- **Installed plist absent** — the bridge isn't installed; run **mcp-bridge-setup** (it generates the
plist from `mcp/vault-server/vault.service.json` via `scripts/gen-launchd.mjs`, resolving
`COS_VAULT_DIR = ${VAULT_DIR}` from `config/load-config.sh` — see `mcp/CLAUDE.md`; the API key is NOT
a plist placeholder — it lives in `config/secrets.env`, loaded by `launch.sh`). From `cos-setup`, that
step runs after this.
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!