Attach a code repo's ψ vault to a caretaker oracle — check, link, heal, unlink. Use when a plain code repo (open-source, not an oracle) should keep memory in another oracle's vault, when the user says "psi check", "psi link", "share the vault", "symlink ψ to neo", "who takes care of this repo's memory", or when a ψ symlink went missing after a checkout. Do NOT trigger for creating an oracle (use /awaken), cloning repos for development (use /incubate), or writing a retrospective (use /rrr).
Installs into .claude/skills of the current project.
Are you the author of Psi?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/soul-brews-studio-psi)
---
name: psi
description: Attach a code repo's ψ vault to a caretaker oracle — check, link, heal, unlink. Use when a plain code repo (open-source, not an oracle) should keep memory in another oracle's vault, when the user says "psi check", "psi link", "share the vault", "symlink ψ to neo", "who takes care of this repo's memory", or when a ψ symlink went missing after a checkout. Do NOT trigger for creating an oracle (use /awaken), cloning repos for development (use /incubate), or writing a retrospective (use /rrr).
argument-hint: "[check | link | heal | unlink]"
---
# /psi
> "A code repo does not need to be an oracle to have a memory."
Point a plain code repo's `ψ` at a **caretaker oracle's** vault, so the repo keeps a brain
without becoming an oracle and without committing vault content into public source history.
```text
/psi # same as check
/psi check # what is ψ here, who takes care of it, is it safe
/psi link # ask which oracle, then absorb → symlink → ignore → verify
/psi heal # symlink vanished after a checkout, or ignore rules incomplete
/psi unlink # go back to a standalone real ψ
```
## The model
```
code repo/ψ ──symlink──▶ caretaker oracle/ψ
(ignored, never committed) (tracked — it IS the brain)
```
| Repo kind | ψ | git |
|---|---|---|
| **code repo** — software someone else could clone and build | symlink to caretaker | **ignored, never tracked** |
| **oracle repo** — has an oracle identity | real directory | **tracked** |
## Pick the caretaker — show, don't guess
Never assume which oracle takes care of a repo. **Show the fleet and let the human name
it.** Linking to the wrong vault mixes two oracles' memory.
```bash
maw ls
```
Ask: *"Which oracle should take care of this repo's memory?"* The human answers with a
name (`neo`, `pulse`, `beta`). Resolve it — the short name works:
```bash
maw locate "$ORACLE_NAME" # prints: repo: /path/to/<name>-oracle and ψ/: present
```
Take the `repo:` line as `CARETAKER`. If `maw locate` finds nothing, or its `ψ/` is not
`present`, stop and say so — do not fall back to a guess.
Do **not** loop over repos or scan the filesystem looking for candidates. One repo, one
question, one answer.
## `check`
Report what you verified, never what you assume. Every line is the output of a command —
`readlink -f` for the real target, `ls-files -s` for the mode bits, `check-ignore -v` for
the rule that actually matched. Close with one **FOCUS** line: the single next action.
```bash
REPO=$(git rev-parse --show-toplevel) || exit 1
GI="$REPO/.gitignore"
# ── ψ: type, size, staleness ─────────────────────────────────────────────
if [ -L "$REPO/ψ" ]; then
TARGET=$(readlink "$REPO/ψ"); REAL=$(readlink -f "$REPO/ψ")
[ -e "$REPO/ψ/" ] && ALIVE=alive || ALIVE='DANGLING'
case "$TARGET" in /*) ABS=' ⚠️ absolute';; *) ABS='';; esac
PSI="symlink → $TARGET$ABS"; PSI2="resolves $REAL ($ALIVE)"
elif [ -d "$REPO/ψ" ]; then
N=$(find "$REPO/ψ" -type f ! -name '.DS_Store' | wc -l | tr -d ' ')
NEW=$(find "$REPO/ψ" -type f ! -name '.DS_Store' -exec stat -f '%m' {} \; 2>/dev/null | sort -rn | head -1)
AGE=$(( ( $(date +%s) - ${NEW:-$(date +%s)} ) / 86400 ))
PSI="real dir · $N files · newest $(date -r "${NEW:-0}" +%F 2>/dev/null) (${AGE}d old)"; PSI2=""
else
PSI="absent"; PSI2=""
fi
# ── git: is any of it tracked? is the LINK itself committed? ─────────────
TRACKED=$(git -C "$REPO" ls-files ψ | wc -l | tr -d ' ')
git -C "$REPO" ls-files -s ψ | rg -q '^120000' && BLOB='⚠️ SYMLINK COMMITTED' || BLOB=no
# ── ignore: both forms, and the rule git actually matched ───────────────
rg -qx 'ψ' "$GI" 2>/dev/null && BARE=yes || BARE='NO ⚠️'
rg -qx 'ψ/' "$GI" 2>/dev/null && SLASH=yes || SLASH=NO
# cut -f1, NOT an awk positional field ref — see "Never use positional parameters"
# below. check-ignore -v separates source from pathname with a TAB, so field 1 is
# the matching rule.
RULE=$(git -C "$REPO" check-ignore -v ψ 2>/dev/null | cut -f1)
[ -n "$RULE" ] || RULE='NOT IGNORED ⚠️'
# ── kind + caretaker ────────────────────────────────────────────────────
[ "$TRACKED" -gt 0 ] && KIND='oracle repo (ψ is tracked — it IS the brain)' \
|| KIND='code repo (ψ must stay ignored)'
# NOTE: no \K — this rg build rejects it, and the error would silently read as "none".
CARE=$(sed -n 's/^oracle: *//p' "$REPO/.claude/PSI_CARETAKER" 2>/dev/null)
[ -n "$CARE" ] || CARE='none recorded'
# ── render: no borders. two-space margin, aligned columns, grouped by blank
# lines. identity block first, then the checks, then one FOCUS line.
# Each check names the command that proved it — the report is its own audit.
# NOTE: printf inline per row — no row()/fact() helpers, because a helper
# would need positional parameters and the host rewrites those. See below.
F=' %-14s%s\n' # identity line
R=' %-14s%-42s%-3s %s\n' # check line
echo
printf "$F" repo "$(basename "$REPO")"
printf "$F" kind "$KIND"
printf "$F" ψ "$PSI"
[ -n "$PSI2" ] && printf "$F" '' "$PSI2"
echo
printf "$R" tracked "$TRACKED entries" "$([ "$TRACKED" -eq 0 ] && echo ✓ || echo ⚠)" 'git ls-files ψ'
printf "$R" symlink-blob "$BLOB" "$([ "$BLOB" = no ] && echo ✓ || echo ⚠)" 'ls-files -s ψ → 120000'
printf "$R" bare-ψ "$BARE" "$([ "$BARE" = yes ] && echo ✓ || echo ⚠)" 'rg -qx ψ .gitignore'
printf "$R" 'ψ/' "$SLASH" "$([ "$SLASH" = yes ] && echo ✓ || echo ·)" 'rg -qx ψ/ .gitignore'
printf "$R" ignore-rule "$RULE" "$(case $RULE in *NOT*) echo ⚠;; *) echo ✓;; esac)" 'git check-ignore -v ψ'
printf "$R" caretaker "$CARE" "$([ "$CARE" = 'none recorded' ] && echo · || echo ✓)" '.claude/PSI_CARETAKER'
echo
```
Keep the two-space margin and the blank-line grouping — the whitespace is what makes it
readable without rules. Widen a column only if a value would otherwise wrap; never add
borders back.
### Never use positional parameters (dollar-digit) in this skill's shell
The host substitutes the invocation's positional arguments into the skill body before the
model ever sees it. Running `/psi link to neo but …` rewrote an `awk` program that
referenced field one into `awk '{print to}'`, and a four-parameter `printf` helper into
`printf … "to" "neo" "but" "when"` — silent corruption, no error.
So: no shell functions taking positional parameters, and no dollar-digit inside
`awk`/`sed` programs. Use `cut -f1`, a named variable, or a printf format string held in
a variable — all three are immune.
### Deciding FOCUS
Emit exactly one, first match wins — most dangerous first:
| Condition | FOCUS |
|---|---|
| `symlink-blob` committed | **leaking a machine path in git history** → `/psi heal`, then decide on a history rewrite |
| symlink `DANGLING` | **brain unreachable** → `/psi heal` |
| symlink target absolute | **breaks on every other machine** → `/psi heal` (rewrites relative) |
| code repo & `bare-ψ=NO` | **ψ will leak the moment it is linked** → add the bare rule |
| code repo & `NOT IGNORED` | **do not commit** → fix `.gitignore` first |
| code repo, real dir, no caretaker | **orphaned vault — no oracle reads these N files** → `/psi link` |
| oracle repo, ψ tracked | ✅ correct — the vault belongs here, nothing to do |
| symlink alive, relative, ignored | ✅ linked and safe |
A stale `newest` date is worth naming even on a ✅ — a vault whose newest file is weeks old
is why a later `/recap` will hand back a stale handoff as if it were current.
## `link` — a six-phase ritual
Run it **step by step**, not as one script. Announce each phase as you enter it, do that
phase's work, show the result, then continue. Two phases are hard **gates**: stop and wait
for a human answer. After the phases, run the checklist, then print the report.
```text
Phase 1/6 · Survey what ψ is now
Phase 2/6 · Caretaker ← GATE: the human names the oracle
Phase 3/6 · Preview ← GATE: the human approves the dry run
Phase 4/6 · Absorb copy + count both sides
Phase 5/6 · Link park → symlink → verify through it
Phase 6/6 · Seal ignore rules + caretaker record
```
Never run a later phase's commands while announcing an earlier one. A gate that is
"announced and then passed in the same breath" is not a gate.
### Phase 1/6 · Survey — refuse bad ground
Run `check` first and show it. Then stop, with the reason, when:
- the repo is itself an oracle (ψ tracked with real content),
- ψ already points at that same caretaker → say `already linked`, exit 0,
- there is no origin remote (the vault path cannot be derived).
### Phase 2/6 · Caretaker — GATE
Show `maw ls`, then **ask which oracle should take care of this repo's memory and wait for
the answer.** Resolve it with `maw locate`, and confirm the caretaker's `ψ/: present` plus
that it is not behind its remote. Never pick a name yourself, never carry one over from a
previous run.
### Phase 3/6 · Preview — GATE
Show the itemized dry run and the counts, then **wait for approval.** Dry-run output is not
consent. If the human says nothing, nothing happens.
### Phase 4/6 · Absorb — dry run first, always
Replacing a populated ψ with a symlink orphans everything inside it. Fetch the caretaker
first so a stale vault is not merged over newer content.
Every linked repo lives under **`ψ/family/<host>/<owner>/<repo>`**, always lowercased.
Below `family/` the path is the ghq tree exactly, so it reads the same in both places:
```text
/opt/Code/github.com/soul-brews-studio/arra-oracle-skills-cli ← the code (ghq)
neo-oracle/ψ/family/github.com/soul-brews-studio/arra-oracle-skills-cli ← its memory
└ family ┘└─ host ──┘└─ owner ───────┘└─ repo ────────────┘
```
**`family/` keeps the oracle's kin clear of its own organs.** Without it an owner directory
lands beside `memory/`, `inbox/`, `teams/` — and an org literally named `teams` collides
with the oracle's own. One directory to list to see everything an oracle tends.
**The host is derived, never assumed.** `gitlab.com`, `codeberg.org`, or a self-hosted
`git.example.com` each get their own subtree, so two repos sharing an `owner/name` on
different hosts never collide.
Lowercase is not cosmetic either: `Soul-Brews-Studio` and `soul-brews-studio` are the same
repo, but on a case-sensitive volume they become two vaults, and the split is invisible
until memory goes missing.
```bash
git -C "$CARETAKER" fetch --quiet 2>/dev/null
# host/owner/repo from the origin remote, lowercased — never from the directory
# name, which may have been renamed locally. Handles https, ssh, scp-style,
# git://, an embedded user, a custom port, and gitlab subgroups.
URL=$(git -C "$REPO" remote get-url origin 2>/dev/null)
[ -n "$URL" ] || { echo "✗ no origin remote — cannot derive the vault path"; exit 1; }
SLUG=$(printf '%s' "$URL" | sed -E \
-e 's#^(ssh|git\+ssh|https?|git)://##' \
-e 's#^[^@/]+@##' \
-e 's#:[0-9]+/#/#' \
-e 's#:#/#' \
-e 's#\.git$##' \
-e 's#/+$##' \
| tr '[:upper:]' '[:lower:]')
NS="$CARETAKER/ψ/family/$SLUG" # ψ/family/<host>/<owner>/<repo>, lowercase
rsync -a --dry-run --itemize-changes "$REPO/ψ/" "$NS/"
```
Verified against every remote shape:
| remote | slug |
|---|---|
| `https://github.com/Soul-Brews-Studio/arra-oracle-skills-cli` | `github.com/soul-brews-studio/arra-oracle-skills-cli` |
| `git@github.com:laris-co/Neo-Oracle.git` | `github.com/laris-co/neo-oracle` |
| `git@gitlab.com:MyGroup/sub/proj.git` | `gitlab.com/mygroup/sub/proj` |
| `ssh://git@git.example.com:2222/team/thing.git` | `git.example.com/team/thing` |
| `git://codeberg.org/Owner/Proj.git` | `codeberg.org/owner/proj` |
Print the itemized list and the file count, then **wait for approval**. Only then re-run
without `--dry-run`. Never `--delete`.
The absorb and the removal are never the same command, and never the same step. Step 3
sequences them: **copy → count both sides → park the original → link → verify through the
link**, with an automatic rollback if the last check fails.
### Phase 5/6 · Link — copy, verify, park, link, verify again
**Order is the safety.** The source is never removed; it is *parked* outside the repo and
only after the copy has been counted. If the link fails to resolve, roll back automatically.
Never `rm -rf` the vault — see "nothing deleted".
```bash
mkdir -p "$NS"
# 3a. copy for real (same command as the dry run, minus --dry-run)
rsync -a --exclude '.DS_Store' "$REPO/ψ/" "$NS/"
# 3b. VERIFY before anything moves. Counts must match exactly, or stop —
# the source is still untouched at this point, so aborting costs nothing.
SRC_N=$(find "$REPO/ψ" -type f ! -name '.DS_Store' | wc -l | tr -d ' ')
DST_N=$(find "$NS" -type f ! -name '.DS_Store' | wc -l | tr -d ' ')
if [ "$SRC_N" -ne "$DST_N" ]; then
echo "✗ ABORT — copied $DST_N of $SRC_N files. Source untouched; nothing removed."
exit 1
fi
echo "✓ verified $DST_N/$SRC_N files copied"
# 3c. PARK the original outside the repo (never delete, never leave it in git's way)
PARK="${TMPDIR:-/tmp}/psi-replaced-$(echo "$SLUG" | tr / -)-$(date +%Y%m%d-%H%M%S)"
mv "$REPO/ψ" "$PARK"
# 3d. link, relative
REL=$(python3 -c 'import os,sys;print(os.path.relpath(sys.argv[1],sys.argv[2]))' "$NS" "$REPO")
ln -sfn "$REL" "$REPO/ψ" # -n: do not descend into an existing symlink
# 3e. verify the link RESOLVES and the content is reachable THROUGH it.
# Any failure restores the parked original and exits.
restore() { rm -f "$REPO/ψ"; mv "$PARK" "$REPO/ψ"; echo "↩ restored original ψ"; }
[ -e "$REPO/ψ/" ] || { echo "✗ link does not resolve"; restore; exit 1; }
THRU=$(find "$REPO/ψ/" -type f ! -name '.DS_Store' | wc -l | tr -d ' ')
[ "$THRU" -ge "$SRC_N" ] || { echo "✗ only $THRU/$SRC_N reachable through the link"; restore; exit 1; }
readlink -f "$REPO/ψ"
echo "✓ $THRU files reachable through the link"
echo " original parked: $PARK (delete it yourself once satisfied)"
```
`ln -sfn`, never `ln -sf` — with a pre-existing symlinked directory `-f` alone creates the
link *inside* the target instead of replacing it.
**Why park instead of delete:** at 3c the content exists in two places and has been counted
in both, so parking looks redundant — until 3d writes a link that silently resolves
somewhere unexpected. Parking makes 3e's rollback possible. Announce the park path; let the
human delete it. Nothing deleted.
### Phase 6/6 · Seal — ignore it, prove it, record the caretaker
```bash
for pat in 'ψ' 'ψ/'; do
rg -qx "$pat" "$REPO/.gitignore" 2>/dev/null || echo "$pat" >> "$REPO/.gitignore"
done
if git -C "$REPO" ls-files --error-unmatch ψ >/dev/null 2>&1; then
git -C "$REPO" rm --cached -r --quiet ψ
echo "✓ untracked previously-committed ψ (history still holds it — rewrite separately)"
fi
git -C "$REPO" check-ignore -v ψ || echo "⚠️ ψ is NOT ignored — do not commit"
```
Verification is part of the step. If `check-ignore` prints nothing, the link is not safe —
report that instead of declaring success.
#### Record the caretaker
```bash
mkdir -p "$REPO/.claude"
printf 'oracle: %s\nrepo: %s\nvault: %s\ndate: %s\n' \
"$ORACLE_NAME" "$CARETAKER" "$NS" "$(date +%F)" > "$REPO/.claude/PSI_CARETAKER"
rg -qx '.claude/PSI_CARETAKER' "$REPO/.gitignore" 2>/dev/null \
|| echo '.claude/PSI_CARETAKER' >> "$REPO/.gitignore"
```
Machine-local, ignored like the link. `check` reads it to name the caretaker without asking
again.
### Checklist — run it, show it, and let it fail loudly
After phase 6, verify the outcome. Every line re-runs a real command; nothing is asserted
from memory of what the phases *intended* to do. Print it. Unlike the retro checklist in
`/rrr`, this one is **shown** — it is the evidence that a data move succeeded.
```bash
C=' %-2s %-34s%s\n' # format in a variable — no helper, no positional params
# 1 counts match on both sides
SRC_OK=$([ "${SRC_N:-0}" -eq "$DST_N" ] && echo ✓ || echo ✗)
printf "$C" "$SRC_OK" "files copied" "$DST_N/$SRC_N"
# 2 reachable THROUGH the link, not just present in the vault
printf "$C" "$([ "${THRU:-0}" -ge "${SRC_N:-0}" ] && echo ✓ || echo ✗)" "reachable through ψ" "$THRU"
# 3 the link resolves
printf "$C" "$([ -e "$REPO/ψ/" ] && echo ✓ || echo ✗)" "link resolves" "$(readlink -f "$REPO/ψ")"
# 4 relative, not absolute
case "$(readlink "$REPO/ψ")" in /*) A=✗;; *) A=✓;; esac
printf "$C" "$A" "relative target" "$(readlink "$REPO/ψ")"
# 5 lowercase, nested owner/repo
printf "$C" "$([ "$SLUG" = "$(echo "$SLUG" | tr '[:upper:]' '[:lower:]')" ] && echo ✓ || echo ✗)" "vault path lowercase" "ψ/$SLUG"
# 6 git cannot see it
printf "$C" "$(git -C "$REPO" check-ignore -q ψ && echo ✓ || echo ✗)" "ignored by git" "$(git -C "$REPO" check-ignore -v ψ | cut -f1)"
# 7 the link itself is not committed
printf "$C" "$(git -C "$REPO" ls-files -s ψ | rg -q '^120000' && echo ✗ || echo ✓)" "symlink not committed" "$(git -C "$REPO" ls-files ψ | wc -l | tr -d ' ') tracked"
# 8 the original still exists somewhere
printf "$C" "$([ -d "$PARK" ] && echo ✓ || echo ✗)" "original parked" "$PARK"
```
Any `✗` means **stop and say so** — do not print a success report over a failed checklist.
Items 2 and 8 are the ones that matter most: 2 proves the brain is actually reachable, 8
proves nothing was destroyed.
### Report
Close with a short report in the same borderless style as `check`. State what moved, where
it went, and what the human still owns.
```text
ψ linked · arra-oracle-skills-cli → neo
moved 17 files → neo-oracle/ψ/family/github.com/soul-brews-studio/arra-oracle-skills-cli
symlink ../../laris-co/neo-oracle/ψ/family/github.com/soul-brews-studio/arra-oracle-skills-cli
ignored .gitignore:9:ψ (bare rule — ψ/ no longer matches a symlink)
caretaker neo
parked /var/.../psi-replaced-…-20260820-181051
yours to do delete the parked copy once satisfied
commit the .gitignore change
```
Report only what the checklist verified. If an item failed, the report says what failed and
what state the repo is in — never a clean summary over a broken outcome.
## `heal`
`git checkout` and `git merge` silently delete an **ignored** symlink when they remove the
last tracked sibling at that path — no error is printed. Repair, reporting each fix:
1. `PSI_CARETAKER` exists but `ψ` is missing → recreate the relative symlink.
2. `ψ` is a symlink that `readlink -f` cannot resolve → report the dangling target; do not
invent a replacement.
3. An ignore form is missing → add it.
4. `ψ` is tracked → `git rm --cached -r ψ`.
Print `✓ nothing to heal` when clean.
## `unlink`
Copy the namespaced subtree back into a real `ψ`, replace the symlink, and leave the
caretaker's copy alone — never delete the vault side. Keep the ignore lines unless asked.
## Rules
- Show `maw ls` and let the human name the caretaker. Never guess, never scan for one.
- Ask before the first destructive step; dry-run output is not consent.
- **Nothing deleted.** Never `rm -rf` a vault. Copy → count both sides → park the original
outside the repo → link → verify through the link, rolling back on any failure. The human
deletes the parked copy, not the skill.
- Both `ψ` and `ψ/` in `.gitignore`, every time — a trailing slash matches directories
only, so a `ψ/`-only rule stops matching the moment ψ becomes a symlink.
- Relative symlink targets only — machines disagree on the ghq root.
- Never commit ψ or the symlink in a code repo; never un-track ψ in an oracle repo.
- A shared vault is shared: give each repo its own subtree, and say so when linking.
- Report what you verified (`readlink -f`, `check-ignore`), not what you intended.