You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The cohort now has a shared SocratiCode index on a fifth VM, co-index
(index on the tailnet). It has been live and soaking since 2026-09-12 and
passed its 24h criterion today: 149 consecutive check-ins, zero non-202s, all
collections green, NRestarts=0 on every unit.
Adopting gets this repo two things it does not have today: a semantic index it
does not host (no Docker, no Ollama, no 9 GB image on your VM), and cross-repo search — codebase_search with includeLinked: true answers a
question against every cohort repo in the store, not just this one.
This issue is a suggestion from the notifier repo. No commits have been made
here. Sequence and priority are yours.
projectId is what names the Qdrant collections (codebase_watcher, watcher_symgraph_file, …). Committing it is the point: every checkout
addresses the same collections regardless of where the working tree sits on
disk.
2. Add the six client variables to .claude/settings.json
These five are non-secret and should be committed — they are
self-documenting and travel with the checkout.
QDRANT_API_KEY goes in .claude/settings.local.json, which must be
git-ignored in this repo. Check it — do not assume:
git check-ignore -v .claude/settings.local.json # must print a rule
This paragraph used to assert the file is git-ignored. In
CannObserv/broker it was not — no rule, no global core.excludesfile — and
that repo is public. Four of the five cohort repos carried the rule, which is
exactly what made the assertion read as true. Following this issue as
originally written would have put the cohort's single Qdrant key one git add -A from GitHub, and per #57 a leak anywhere is a rotation
everywhere with no overlap window. Reported as notifier#68; broker's own
half is CannObserv/broker#18.
If the rule is missing, add it before installing the key. Do not write the
key by hand — use notifier's scripts/install_qdrant_key.sh,
which is the reviewed home for this step and now refuses any target git would
commit. See Prerequisites below.
3. Create sibling link stubs — not clones
Corrected 2026-09-14. This section originally said "clone the sibling
repos read-only". That is wasteful and slightly wrong; what follows is what
the tool actually needs.
includeLinked resolves each entry in linkedProjects to a path on disk and
uses it for exactly two things:
searchMultipleCollections then receives only {name, label} — no path
reaches it. Search content comes wholly from Qdrant. The directory is how the
tool learns which collection to query, nothing more.
So each sibling needs a directory containing one file:
No checkout, no fetch, no disk, nothing to keep in sync. Notifier's VM now
carries exactly this for archiver, replicator and watcher; copy the README.md
from there if you want the rationale sitting next to the stub.
Verified, not assumed. The real broker clone on notifier's VM was moved
aside and replaced with a directory holding a single 30-byte .socraticode.json. codebase_search returned broker source with correct file
paths and line numbers, labeled [broker], with zero bytes of broker source on
disk.
A stub is also safer than a clone. A real checkout carries the sibling's own .socraticode.json, so if that repo later changes its projectId, a stale
clone silently resolves to the old collection. A stub you wrote is a local
declaration and cannot drift behind a git pull nobody ran.
A stub naming a collection that does not exist yet is safe. searchMultipleCollections catches per-collection failures and skips them, so
search keeps working while the other siblings adopt. The skip is logged to the
server's stderr and not surfaced in the tool result — so, as with trap 1, a
green result is not evidence every sibling answered.
4. Adopt the health hook
Copy .claude/hooks/socraticode-health.sh from notifier and wire it as a SessionStart hook. It reports once per UTC day, never repairs, and exits 0 on
every condition so it cannot take a session down.
Traps
Every one of these was hit or measured during notifier's build. None is
speculative.
1. A missing linked directory is dropped silently.loadLinkedProjects in socraticode/dist/config.js filters on fs.existsSync with no warning. Until
2026-09-14 notifier's .socraticode.json listed four siblings while only ../broker existed on its VM, so includeLinked: true had been quietly
searching one repo, not four, and reporting nothing amiss. Stubs for the other
three are now in place there. Verify your own stub set rather than trusting a
green result.
2. Adopting projectId renames your collections. Resolution order is SOCRATICODE_PROJECT_ID env > .socraticode.json > SHA-256 of the absolute
path. If this repo was ever indexed without the config file, it is sitting in
path-hash collections that the rename orphans. Run codebase_remove, codebase_graph_remove and codebase_context_removebefore adding the
file, then re-index — or the old set lingers in the shared store forever, with
a name nothing maps back to a repo.
3. QDRANT_URL, never QDRANT_HOST.QDRANT_MODE=external refuses to
start without a URL, and the fallback it would otherwise build is `${QDRANT_API_KEY ? "https" : "http"}://${QDRANT_HOST}:${QDRANT_PORT}`
with QDRANT_PORT defaulting to 16333, not 6333. An API key with no URL
therefore assumes https against the wrong port, and the error reads like a
network fault.
4. Use the full MagicDNS name.https://index.taild0fb76.ts.net:6333, not https://index:6333 — the short name is not in the certificate's SAN. Qdrant
serves TLS because SocratiCode refuses to send QDRANT_API_KEY over a
non-TLS, non-localhost connection (D14; this killed notifier's first index
attempt 20 s in).
5. Never set QDRANT_COLLECTION_PREFIX or SOCRATICODE_BRANCH_AWARE=true.
The prefix is prepended to the globalsocraticode_metadata collection too,
so one VM setting it splits the cohort namespace silently. Branch-awareness
appends the branch name to the project id — a fresh collection set per branch.
Both are failure modes every health check still calls green. notifier pins this
with a test (tests/deploy/test_socraticode_config.py); consider the same.
6. The env block applies only in a trusted folder. On a freshly cloned
VM, an untrusted workspace means the block does not apply, QDRANT_MODE
reverts to managed and OLLAMA_MODE to auto — and SocratiCode then tries
to start Docker containers on a host that has none, rather than reporting
missing configuration. Trust the folder, then confirm codebase_health names
the external endpoints and not a container.
7. The health hook skips a configured project with no toolchain — silently. gregoryfoster/skills#281:
9 days, 5 sessions, 0 session lines. Until that is fixed, a green session start
is not evidence the hook ran.
8. Ollama on index has no authentication. Port 11434 is reachable by any
node the ACL admits. It is an embedding endpoint on a private tailnet, which is
the accepted posture — but it is not authenticated, so do not treat tailnet
reachability as authorization.
Prerequisites — operator, tracked in notifier#57
These are not yours to do; listed so a failure is recognizable rather than
mysterious.
Tailnet ACL must admit this repo's VM to tag:index:6333,11434. A peer is
visible only through an acls rule, and a missing one presents as a DNS
failure, not a permission denial (archiver#193, replicator#88).
QDRANT_API_KEY delivered to this VM out of band. Qdrant holds a single
global service.api_key — no key list, no per-client identity, no
per-collection scope — so every cohort VM holds the same secret and a leak
anywhere is a rotation everywhere. Rotation has no overlap window: every
client 401s from the restart until it is updated.
Use notifier's scripts/install_qdrant_key.sh rather than writing the value
by hand. Run from an operator machine, the only host able to reach both ends
— exe.dev VMs are isolated from each other, and Visual diff: screenshot comparison between snapshots #57 D13's tag:index:22 edge
was retired after the soak:
The key travels on stdin, never argv — an argument is visible in ps to
every other process for the life of the call and lands in the caller's shell
history. The script merges into the existing JSON atomically at 0600, so a
crash cannot leave a truncated key.
Expect installed 64 chars. Any other length is a truncated transfer,
which 401s exactly like a wrong key; the length is the only cheap
discriminator between the two. And never run it under bash -x — tracing a
script that touches a credential writes the value to stdout, which is how two
were leaked during Visual diff: screenshot comparison between snapshots #57.
Full walkthrough including rotation: notifier's docs/COMMANDS.md.
Acceptance
git check-ignore -v .claude/settings.local.json prints a rule
install_qdrant_key.sh reported 64 chars
codebase_health reports the external Qdrant and Ollama, no containers
codebase_index completes; collections named codebase_watcher etc. appear in the shared store
any pre-existing path-hash collections for this repo are removed
codebase_search answers a question about this repo
a link stub exists for every entry in linkedProjects
codebase_search with includeLinked: true returns hits from at least one sibling
the health hook prints on a fresh session (or is confirmed skipped per trap 7)
Context
The cohort now has a shared SocratiCode index on a fifth VM,
co-index(
indexon the tailnet). It has been live and soaking since 2026-09-12 andpassed its 24h criterion today: 149 consecutive check-ins, zero non-202s, all
collections
green,NRestarts=0on every unit.docs/plans/2026-09-11-shared-qdrant-vm-design.mdin notifier.socraticode.json+.claude/settings.jsonAdopting gets this repo two things it does not have today: a semantic index it
does not host (no Docker, no Ollama, no 9 GB image on your VM), and
cross-repo search —
codebase_searchwithincludeLinked: trueanswers aquestion against every cohort repo in the store, not just this one.
This issue is a suggestion from the notifier repo. No commits have been made
here. Sequence and priority are yours.
1. Add
.socraticode.jsonat the repo root{ "projectId": "watcher", "linkedProjects": [ "../archiver", "../broker", "../replicator", "../notifier" ] }projectIdis what names the Qdrant collections (codebase_watcher,watcher_symgraph_file, …). Committing it is the point: every checkoutaddresses the same collections regardless of where the working tree sits on
disk.
2. Add the six client variables to
.claude/settings.json{ "env": { "QDRANT_MODE": "external", "QDRANT_URL": "https://index.taild0fb76.ts.net:6333", "OLLAMA_MODE": "external", "OLLAMA_URL": "http://index:11434", "EMBEDDING_MODEL": "nomic-embed-text", "EMBEDDING_DIMENSIONS": "768" } }These five are non-secret and should be committed — they are
self-documenting and travel with the checkout.
QDRANT_API_KEYgoes in.claude/settings.local.json, which must begit-ignored in this repo. Check it — do not assume:
git check-ignore -v .claude/settings.local.json # must print a ruleIf the rule is missing, add it before installing the key. Do not write the
key by hand — use notifier's
scripts/install_qdrant_key.sh,which is the reviewed home for this step and now refuses any target git would
commit. See Prerequisites below.
3. Create sibling link stubs — not clones
includeLinkedresolves each entry inlinkedProjectsto a path on disk anduses it for exactly two things:
searchMultipleCollectionsthen receives only{name, label}— no pathreaches it. Search content comes wholly from Qdrant. The directory is how the
tool learns which collection to query, nothing more.
So each sibling needs a directory containing one file:
No checkout, no fetch, no disk, nothing to keep in sync. Notifier's VM now
carries exactly this for archiver, replicator and watcher; copy the
README.mdfrom there if you want the rationale sitting next to the stub.
Verified, not assumed. The real broker clone on notifier's VM was moved
aside and replaced with a directory holding a single 30-byte
.socraticode.json.codebase_searchreturned broker source with correct filepaths and line numbers, labeled
[broker], with zero bytes of broker source ondisk.
A stub is also safer than a clone. A real checkout carries the sibling's own
.socraticode.json, so if that repo later changes itsprojectId, a staleclone silently resolves to the old collection. A stub you wrote is a local
declaration and cannot drift behind a
git pullnobody ran.A stub naming a collection that does not exist yet is safe.
searchMultipleCollectionscatches per-collection failures and skips them, sosearch keeps working while the other siblings adopt. The skip is logged to the
server's stderr and not surfaced in the tool result — so, as with trap 1, a
green result is not evidence every sibling answered.
4. Adopt the health hook
Copy
.claude/hooks/socraticode-health.shfrom notifier and wire it as aSessionStarthook. It reports once per UTC day, never repairs, and exits 0 onevery condition so it cannot take a session down.
Traps
Every one of these was hit or measured during notifier's build. None is
speculative.
1. A missing linked directory is dropped silently.
loadLinkedProjectsinsocraticode/dist/config.jsfilters onfs.existsSyncwith no warning. Until2026-09-14 notifier's
.socraticode.jsonlisted four siblings while only../brokerexisted on its VM, soincludeLinked: truehad been quietlysearching one repo, not four, and reporting nothing amiss. Stubs for the other
three are now in place there. Verify your own stub set rather than trusting a
green result.
2. Adopting
projectIdrenames your collections. Resolution order isSOCRATICODE_PROJECT_IDenv >.socraticode.json> SHA-256 of the absolutepath. If this repo was ever indexed without the config file, it is sitting in
path-hash collections that the rename orphans. Run
codebase_remove,codebase_graph_removeandcodebase_context_removebefore adding thefile, then re-index — or the old set lingers in the shared store forever, with
a name nothing maps back to a repo.
3.
QDRANT_URL, neverQDRANT_HOST.QDRANT_MODE=externalrefuses tostart without a URL, and the fallback it would otherwise build is
`${QDRANT_API_KEY ? "https" : "http"}://${QDRANT_HOST}:${QDRANT_PORT}`with
QDRANT_PORTdefaulting to 16333, not 6333. An API key with no URLtherefore assumes https against the wrong port, and the error reads like a
network fault.
4. Use the full MagicDNS name.
https://index.taild0fb76.ts.net:6333, nothttps://index:6333— the short name is not in the certificate's SAN. Qdrantserves TLS because SocratiCode refuses to send
QDRANT_API_KEYover anon-TLS, non-localhost connection (D14; this killed notifier's first index
attempt 20 s in).
5. Never set
QDRANT_COLLECTION_PREFIXorSOCRATICODE_BRANCH_AWARE=true.The prefix is prepended to the global
socraticode_metadatacollection too,so one VM setting it splits the cohort namespace silently. Branch-awareness
appends the branch name to the project id — a fresh collection set per branch.
Both are failure modes every health check still calls green. notifier pins this
with a test (
tests/deploy/test_socraticode_config.py); consider the same.6. The
envblock applies only in a trusted folder. On a freshly clonedVM, an untrusted workspace means the block does not apply,
QDRANT_MODEreverts to
managedandOLLAMA_MODEtoauto— and SocratiCode then triesto start Docker containers on a host that has none, rather than reporting
missing configuration. Trust the folder, then confirm
codebase_healthnamesthe external endpoints and not a container.
7. The health hook skips a configured project with no toolchain — silently.
gregoryfoster/skills#281:
9 days, 5 sessions, 0 session lines. Until that is fixed, a green session start
is not evidence the hook ran.
8. Ollama on
indexhas no authentication. Port 11434 is reachable by anynode the ACL admits. It is an embedding endpoint on a private tailnet, which is
the accepted posture — but it is not authenticated, so do not treat tailnet
reachability as authorization.
Prerequisites — operator, tracked in notifier#57
These are not yours to do; listed so a failure is recognizable rather than
mysterious.
Tailnet ACL must admit this repo's VM to
tag:index:6333,11434. A peer isvisible only through an
aclsrule, and a missing one presents as a DNSfailure, not a permission denial (archiver#193, replicator#88).
QDRANT_API_KEYdelivered to this VM out of band. Qdrant holds a singleglobal
service.api_key— no key list, no per-client identity, noper-collection scope — so every cohort VM holds the same secret and a leak
anywhere is a rotation everywhere. Rotation has no overlap window: every
client 401s from the restart until it is updated.
Use notifier's
scripts/install_qdrant_key.shrather than writing the valueby hand. Run from an operator machine, the only host able to reach both ends
— exe.dev VMs are isolated from each other, and Visual diff: screenshot comparison between snapshots #57 D13's
tag:index:22edgewas retired after the soak:
The key travels on stdin, never argv — an argument is visible in
pstoevery other process for the life of the call and lands in the caller's shell
history. The script merges into the existing JSON atomically at 0600, so a
crash cannot leave a truncated key.
Expect
installed 64 chars. Any other length is a truncated transfer,which 401s exactly like a wrong key; the length is the only cheap
discriminator between the two. And never run it under
bash -x— tracing ascript that touches a credential writes the value to stdout, which is how two
were leaked during Visual diff: screenshot comparison between snapshots #57.
Full walkthrough including rotation: notifier's
docs/COMMANDS.md.Acceptance
git check-ignore -v .claude/settings.local.jsonprints a ruleinstall_qdrant_key.shreported 64 charscodebase_healthreports the external Qdrant and Ollama, no containerscodebase_indexcompletes; collections namedcodebase_watcheretc. appear in the shared storecodebase_searchanswers a question about this repolinkedProjectscodebase_searchwithincludeLinked: truereturns hits from at least one sibling